Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions acdc-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
source location via inline anchors. This goes beyond asciidoctor's HTML backend which
leaves index sections empty. The index only renders when it's the last section in the
document.
- **Semantic HTML5 backend** — `--backend html5s` produces semantic HTML5 output with proper
elements and ARIA roles instead of div-based layout. ([#329])

### Fixed

Expand Down Expand Up @@ -52,6 +54,7 @@ This is tagged but unreleased in crates.io for now.
[#272]: https://github.com/nlopes/acdc/issues/272
[#273]: https://github.com/nlopes/acdc/issues/273
[#311]: https://github.com/nlopes/acdc/issues/311
[#329]: https://github.com/nlopes/acdc/issues/329

[Unreleased]: https://github.com/nlopes/acdc/compare/acdc-cli-v0.1.0...HEAD
[0.1.0]: https://github.com/nlopes/acdc/releases/tag/acdc-cli-v0.1.0
18 changes: 8 additions & 10 deletions acdc-cli/src/subcommands/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,18 @@ pub fn run(args: &Args) -> miette::Result<()> {
.timings(args.timings)
.embedded(args.embedded)
.output_destination(output_destination)
.backend(args.backend)
.build();

match args.backend {
#[cfg(feature = "html")]
Backend::Html => {
// HTML can process files in parallel - each file writes to separate output
run_processor::<acdc_converters_html::Processor>(
args,
options,
document_attributes,
true,
)
.map_err(|e| error::display(&e))
}
Backend::Html | Backend::Html5s => run_processor::<acdc_converters_html::Processor>(
args,
options,
document_attributes,
true,
)
.map_err(|e| error::display(&e)),

#[cfg(feature = "terminal")]
Backend::Terminal => {
Expand Down
1 change: 1 addition & 0 deletions converters/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive module-level documentation
- `acdc-converters-dev` crate for test utilities (not published to crates.io)
- Visitor method `visit_callout_ref` for processing callout references
- `Backend::Html5s` variant for semantic HTML5 output

### Fixed

Expand Down
9 changes: 8 additions & 1 deletion converters/core/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum Backend {
/// HTML output format.
#[default]
Html,
/// Semantic HTML5 output format (html5s).
Html5s,
/// Unix manpage (roff/troff) output format.
Manpage,
/// Terminal/console output with ANSI formatting.
Expand All @@ -24,10 +26,11 @@ impl FromStr for Backend {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"html" => Ok(Self::Html),
"html5s" => Ok(Self::Html5s),
"manpage" => Ok(Self::Manpage),
"terminal" => Ok(Self::Terminal),
_ => Err(format!(
"invalid backend: '{s}', expected: html, manpage, terminal"
"invalid backend: '{s}', expected: html, html5s, manpage, terminal"
)),
}
}
Expand All @@ -37,6 +40,7 @@ impl std::fmt::Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Html => write!(f, "html"),
Self::Html5s => write!(f, "html5s"),
Self::Manpage => write!(f, "manpage"),
Self::Terminal => write!(f, "terminal"),
}
Expand All @@ -52,6 +56,8 @@ mod tests {
fn test_from_str() {
assert_eq!(Backend::from_str("html").unwrap(), Backend::Html);
assert_eq!(Backend::from_str("HTML").unwrap(), Backend::Html);
assert_eq!(Backend::from_str("html5s").unwrap(), Backend::Html5s);
assert_eq!(Backend::from_str("HTML5S").unwrap(), Backend::Html5s);
assert_eq!(Backend::from_str("manpage").unwrap(), Backend::Manpage);
assert_eq!(Backend::from_str("terminal").unwrap(), Backend::Terminal);
assert!(Backend::from_str("invalid").is_err());
Expand All @@ -60,6 +66,7 @@ mod tests {
#[test]
fn test_display() {
assert_eq!(Backend::Html.to_string(), "html");
assert_eq!(Backend::Html5s.to_string(), "html5s");
assert_eq!(Backend::Manpage.to_string(), "manpage");
assert_eq!(Backend::Terminal.to_string(), "terminal");
}
Expand Down
16 changes: 16 additions & 0 deletions converters/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pub struct Options {
embedded: bool,
/// Output destination for conversion.
output_destination: OutputDestination,
backend: Backend,
}

impl Options {
Expand Down Expand Up @@ -218,6 +219,12 @@ impl Options {
pub fn output_destination(&self) -> &OutputDestination {
&self.output_destination
}

/// Get the backend type.
#[must_use]
pub fn backend(&self) -> Backend {
self.backend
}
}

/// Builder for [`Options`].
Expand All @@ -231,6 +238,7 @@ pub struct OptionsBuilder {
timings: bool,
embedded: bool,
output_destination: OutputDestination,
backend: Backend,
}

impl OptionsBuilder {
Expand Down Expand Up @@ -281,6 +289,13 @@ impl OptionsBuilder {
self
}

/// Set the backend type.
#[must_use]
pub fn backend(mut self, backend: Backend) -> Self {
self.backend = backend;
self
}

/// Build the [`Options`] instance.
#[must_use]
pub fn build(self) -> Options {
Expand All @@ -291,6 +306,7 @@ impl OptionsBuilder {
timings: self.timings,
embedded: self.embedded,
output_destination: self.output_destination,
backend: self.backend,
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions converters/html/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Semantic HTML5 backend (`html5s`)** — new `--backend html5s` option produces semantic HTML5
using `<section>`, `<aside>`, `<figure>`, ARIA roles, and proper heading hierarchy instead of
the traditional div-based layout. Inspired by Jakub Jirutka's
[asciidoctor-html5s](https://github.com/jirutka/asciidoctor-html5s). Includes dedicated
light and dark mode stylesheets, and supports `html5s-force-stem-type`,
`html5s-image-default-link`, and `html5s-image-self-link-label` document attributes. ([#329])
- **Bibliography list class** - Unordered lists inside `[bibliography]` sections now render
with `class="ulist bibliography"` on the wrapper div and `class="bibliography"` on the
`<ul>` element, matching asciidoctor.
Expand Down Expand Up @@ -102,3 +108,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#291]: https://github.com/nlopes/acdc/issues/291
[#313]: https://github.com/nlopes/acdc/pull/313
[#323]: https://github.com/nlopes/acdc/issues/323
[#329]: https://github.com/nlopes/acdc/issues/329
70 changes: 68 additions & 2 deletions converters/html/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,69 @@ Verbatim blocks (listing, literal) support the `subs` attribute:
- `subs=+attributes` - enable attribute expansion
- `subs=+replacements` - enable typography (arrows, dashes, ellipsis)

### Mathematical Formulas (Stem)

Render mathematical formulas using MathJax 4. Enable it by setting the `:stem:` document attribute.

[source,asciidoc]
....
= My Document
:stem: latexmath
....

The `:stem:` attribute accepts:

- `latexmath` - use LaTeX notation
- `asciimath` - use AsciiMath notation
- _(empty)_ - defaults to `latexmath` for blocks, `asciimath` for inline

**Inline formulas** use the `stem:[]` macro:

[source,asciidoc]
....
The solution is stem:[x = (-b +- sqrt(b^2 - 4ac)) / (2a)].
....

**Block formulas** use the `[stem]` attribute on a passthrough block:

[source,asciidoc]
....
[stem]
++++
x = (-b +- sqrt(b^2 - 4ac)) / (2a)
++++
....

When enabled, MathJax is loaded from the jsdelivr CDN (`cdn.jsdelivr.net/npm/mathjax@4`) and renders formulas client-side. LaTeX uses `\(...\)` and `\[...\]` delimiters; AsciiMath uses `\$...\$`.

To suppress MathJax processing on specific elements, add the CSS class `nostem`, `nolatexmath`, or `noasciimath`.

### Semantic HTML5 Output (`html5s`)

An alternative backend that produces semantic HTML5 instead of the traditional div-based layout, inspired by Jakub Jirutka's https://github.com/jirutka/asciidoctor-html5s[asciidoctor-html5s] gem for Asciidoctor. Enable it with `--backend html5s`:

[source,console]
....
acdc convert --backend html5s document.adoc
....

Key differences from the standard backend:

- **Sections** use `<section>` elements instead of `<div class="sectN">`
- **Admonitions** use `<aside>` (note, tip) or `<section>` (warning, important, caution) with ARIA roles
- **Images** use `<figure>` and `<figcaption>` instead of `<div class="imageblock">`
- **Example blocks** use `<figure>` instead of `<div class="exampleblock">`
- **Sidebars** use `<aside>` instead of `<div class="sidebarblock">`
- **Callout lists** use `<ol>` instead of a table layout
- **Titled paragraphs** are wrapped in `<section>` with `<h6 class="block-title">`
- **Block titles** use `<h6 class="block-title">` instead of `<div class="title">`

The semantic variant ships with its own stylesheet (light and dark mode) and supports a few additional document attributes:

- `:html5s-force-stem-type:` — override the stem notation (`latexmath` or `asciimath`) regardless of the `:stem:` value
- `:html5s-image-default-link: self` — make all images link to themselves by default
- `:html5s-image-self-link-label:` — custom aria label for self-linked images (default: "Open the image in full size")

### Document Attributes

Set document attributes via command line:
Expand Down Expand Up @@ -175,5 +238,8 @@ The HTML converter:

## Differences from Asciidoctor

While the HTML converter aims for compatibility with Asciidoctor, there are some known
differences and limitations.
While the HTML converter aims for compatibility with Asciidoctor, there are some known differences and limitations.

- The `css-signature` attribute is not supported. Use a document ID instead (`[[my-id]]` above the title).
- Syntax highlighting uses syntect with inline CSS styles rather than external highlight.js or Pygments. Language coverage may differ.
- The `html5s` backend is inspired by Jakub Jirutka's https://github.com/jirutka/asciidoctor-html5s[asciidoctor-html5s] gem but is a separate implementation with its own output.
79 changes: 78 additions & 1 deletion converters/html/src/admonition.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use acdc_converters_core::visitor::WritableVisitor;
use acdc_parser::{Admonition, AdmonitionVariant, AttributeValue};

use crate::{Error, Processor};
use crate::{Error, HtmlVariant, Processor};

pub(crate) fn visit_admonition<V: WritableVisitor<Error = Error>>(
visitor: &mut V,
Expand All @@ -27,6 +27,10 @@ pub(crate) fn visit_admonition<V: WritableVisitor<Error = Error>>(
})
.ok_or(Error::InvalidAdmonitionCaption(caption_attr.to_string()))?;

if processor.variant() == HtmlVariant::Semantic {
return visit_admonition_semantic(visitor, admon, caption);
}

let mut writer = visitor.writer_mut();
writeln!(writer, "<div class=\"admonitionblock {}\">", admon.variant)?;
writeln!(writer, "<table>")?;
Expand Down Expand Up @@ -86,3 +90,76 @@ pub(crate) fn visit_admonition<V: WritableVisitor<Error = Error>>(
writeln!(writer, "</div>")?;
Ok(())
}

/// Render an admonition block in semantic HTML5 mode.
fn visit_admonition_semantic<V: WritableVisitor<Error = Error>>(
visitor: &mut V,
admon: &Admonition,
caption: &str,
) -> Result<(), Error> {
// Note/Tip use <aside> with role="note"/"doc-tip"
// Warning/Important/Caution use <section> with role="doc-notice"
let (tag, role) = match admon.variant {
AdmonitionVariant::Note => ("aside", "note"),
AdmonitionVariant::Tip => ("aside", "doc-tip"),
AdmonitionVariant::Warning | AdmonitionVariant::Important | AdmonitionVariant::Caution => {
("section", "doc-notice")
}
};

let mut writer = visitor.writer_mut();
// Build class: "admonition-block {variant}" + roles
let base_class = format!("admonition-block {}", admon.variant);
let class = crate::build_class(&base_class, &admon.metadata.roles);
write!(writer, "<{tag} class=\"{class}\"")?;
// Propagate id
if let Some(id) = &admon.metadata.id {
write!(writer, " id=\"{}\"", id.id)?;
} else if let Some(anchor) = admon.metadata.anchors.first() {
write!(writer, " id=\"{}\"", anchor.id)?;
}
writeln!(writer, " role=\"{role}\">")?;

if admon.title.is_empty() {
// Label-only: no trailing space after colon
writeln!(
writer,
"<h6 class=\"block-title label-only\"><span class=\"title-label\">{caption}:</span></h6>"
)?;
} else {
// With title: single h6 combining label + title (space after colon)
write!(
writer,
"<h6 class=\"block-title\"><span class=\"title-label\">{caption}: </span>"
)?;
let _ = writer;
visitor.visit_inline_nodes(&admon.title)?;
writer = visitor.writer_mut();
writeln!(writer, "</h6>")?;
}
let _ = writer;

// Render content blocks
match admon.blocks.as_slice() {
[acdc_parser::Block::Paragraph(para)] => {
let writer = visitor.writer_mut();
write!(writer, "<p>")?;
let _ = writer;
visitor.visit_inline_nodes(&para.content)?;
let writer = visitor.writer_mut();
writeln!(writer, "</p>")?;
}
[block] => {
visitor.visit_block(block)?;
}
blocks => {
for block in blocks {
visitor.visit_block(block)?;
}
}
}

let writer = visitor.writer_mut();
writeln!(writer, "</{tag}>")?;
Ok(())
}
Loading
Loading