From da706cd13a29a97c8551166153add5244f39fb39 Mon Sep 17 00:00:00 2001 From: Mattias Holm Date: Thu, 26 Feb 2026 18:12:12 +0100 Subject: [PATCH 01/38] Initial work on extended asciidoc This commit adds CLI flags and propagation of XAD options. --- .../Sources/AsciiDocCore/XADOptions.swift | 23 ++ Sources/AsciiDocCore/ISM.swift | 5 +- Sources/AsciiDocCore/Parser.swift | 5 +- Sources/AsciiDocCore/XADOptions.swift | 23 ++ Sources/AsciiDocRender/DocumentRender.swift | 13 +- Sources/AsciiDocRender/FootnoteResolver.swift | 3 +- Sources/asciidoc-swift/Commands.swift | 206 +++++++++++++++++- .../asciidoc-swift.docc/asciidoc-swift.md | 61 ++++++ 8 files changed, 326 insertions(+), 13 deletions(-) create mode 100644 .swiftpm/xcode/AsciiDoc-Swift/Sources/AsciiDocCore/XADOptions.swift create mode 100644 Sources/AsciiDocCore/XADOptions.swift diff --git a/.swiftpm/xcode/AsciiDoc-Swift/Sources/AsciiDocCore/XADOptions.swift b/.swiftpm/xcode/AsciiDoc-Swift/Sources/AsciiDocCore/XADOptions.swift new file mode 100644 index 0000000..9343084 --- /dev/null +++ b/.swiftpm/xcode/AsciiDoc-Swift/Sources/AsciiDocCore/XADOptions.swift @@ -0,0 +1,23 @@ +// +// Copyright (c) 2026 Mattias Holm +// SPDX-License-Identifier: Apache-2.0 +// + +public struct XADOptions: Sendable, Equatable { + public var enabled: Bool + public var strict: Bool + public var pagedJS: Bool + public var templatePath: String? + + public init( + enabled: Bool = false, + strict: Bool = false, + pagedJS: Bool = false, + templatePath: String? = nil + ) { + self.enabled = enabled + self.strict = strict + self.pagedJS = pagedJS + self.templatePath = templatePath + } +} diff --git a/Sources/AsciiDocCore/ISM.swift b/Sources/AsciiDocCore/ISM.swift index 616fbd9..fe7ec25 100644 --- a/Sources/AsciiDocCore/ISM.swift +++ b/Sources/AsciiDocCore/ISM.swift @@ -402,17 +402,20 @@ public struct AdocDocument: Sendable, Equatable { public var header: AdocHeader? = nil public var blocks: [AdocBlock] = [] public var span: AdocRange? + public var xadOptions: XADOptions = .init() public init( attributes: [String: String?] = [:], header: AdocHeader? = nil, blocks: [AdocBlock] = [], - span: AdocRange? = nil + span: AdocRange? = nil, + xadOptions: XADOptions = .init() ) { self.attributes = attributes self.header = header self.blocks = blocks self.span = span + self.xadOptions = xadOptions } } diff --git a/Sources/AsciiDocCore/Parser.swift b/Sources/AsciiDocCore/Parser.swift index b06409a..e965fc7 100644 --- a/Sources/AsciiDocCore/Parser.swift +++ b/Sources/AsciiDocCore/Parser.swift @@ -232,7 +232,8 @@ public struct AdocParser: Sendable { attributes initialAttributes: [String: String?] = [:], lockedAttributeNames: Set = [], includeHeaderDerivedAttributes: Bool = true, - preprocessorOptions: Preprocessor.Options = .init() + preprocessorOptions: Preprocessor.Options = .init(), + xadOptions: XADOptions = .init() ) -> AdocDocument { let preprocessor = Preprocessor(options: preprocessorOptions) let preprocessed = preprocessor.process( @@ -266,7 +267,7 @@ public struct AdocParser: Sendable { return AdocRange(start: first.range.start, end: last.range.end) }() - return AdocDocument(attributes: docAttrs, header: header, blocks: bodyBlocks, span: docSpan) + return AdocDocument(attributes: docAttrs, header: header, blocks: bodyBlocks, span: docSpan, xadOptions: xadOptions) } diff --git a/Sources/AsciiDocCore/XADOptions.swift b/Sources/AsciiDocCore/XADOptions.swift new file mode 100644 index 0000000..9343084 --- /dev/null +++ b/Sources/AsciiDocCore/XADOptions.swift @@ -0,0 +1,23 @@ +// +// Copyright (c) 2026 Mattias Holm +// SPDX-License-Identifier: Apache-2.0 +// + +public struct XADOptions: Sendable, Equatable { + public var enabled: Bool + public var strict: Bool + public var pagedJS: Bool + public var templatePath: String? + + public init( + enabled: Bool = false, + strict: Bool = false, + pagedJS: Bool = false, + templatePath: String? = nil + ) { + self.enabled = enabled + self.strict = strict + self.pagedJS = pagedJS + self.templatePath = templatePath + } +} diff --git a/Sources/AsciiDocRender/DocumentRender.swift b/Sources/AsciiDocRender/DocumentRender.swift index 2bb4e67..6cc3345 100644 --- a/Sources/AsciiDocRender/DocumentRender.swift +++ b/Sources/AsciiDocRender/DocumentRender.swift @@ -12,18 +12,21 @@ public struct RenderConfig { public var xrefResolver: XrefResolver? public var navigationTree: [String: Any]? public var customTemplateName: String? + public var xadOptions: XADOptions public init( backend: Backend, inlineBackend: AdocInlineBackend? = nil, xrefResolver: XrefResolver? = nil, navigationTree: [String: Any]? = nil, - customTemplateName: String? = nil + customTemplateName: String? = nil, + xadOptions: XADOptions = .init() ) { self.backend = backend self.xrefResolver = xrefResolver self.navigationTree = navigationTree self.customTemplateName = customTemplateName + self.xadOptions = xadOptions self.inlineBackend = inlineBackend ?? { switch backend { case .html5: return .html5 @@ -93,7 +96,13 @@ public final class DocumentRenderer { "headerTitle": docToRender.header?.title?.plain ?? "", "blocks": blocks, "footnotes": footnotes.map { renderFootnoteDefinition($0, context: inlineContext) }, - "indexCatalog": indexEntries + "indexCatalog": indexEntries, + "xad": [ + "enabled": config.xadOptions.enabled, + "strict": config.xadOptions.strict, + "pagedJS": config.xadOptions.pagedJS, + "templatePath": config.xadOptions.templatePath ?? "" + ] ] if let nav = config.navigationTree { diff --git a/Sources/AsciiDocRender/FootnoteResolver.swift b/Sources/AsciiDocRender/FootnoteResolver.swift index 3e379e9..8a58fd8 100644 --- a/Sources/AsciiDocRender/FootnoteResolver.swift +++ b/Sources/AsciiDocRender/FootnoteResolver.swift @@ -36,7 +36,8 @@ public final class FootnoteResolver { attributes: document.attributes, header: document.header, blocks: newBlocks, - span: document.span + span: document.span, + xadOptions: document.xadOptions ) return FootnoteResolution(document: newDoc, definitions: collected) diff --git a/Sources/asciidoc-swift/Commands.swift b/Sources/asciidoc-swift/Commands.swift index 7044d09..0b33555 100644 --- a/Sources/asciidoc-swift/Commands.swift +++ b/Sources/asciidoc-swift/Commands.swift @@ -53,7 +53,7 @@ struct AsciiDocSwift: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "asciidoc-swift", abstract: "Swift AsciiDoc implementation with a JSON adapter for the Eclipse TCK.", - subcommands: [JSONAdapter.self, HTML.self, DocBook.self, Latex.self, Lint.self, Antora.self] + subcommands: [JSONAdapter.self, HTML.self, XadHtml.self, XadPagedHtml.self, DocBook.self, Latex.self, Lint.self, Antora.self] ) } @@ -179,6 +179,18 @@ struct HTML: AsyncParsableCommand { @Option(help: "Path to the Stencil templates root directory.") var template: String = "Templates" + @Flag(name: .long, help: "Enable XAD mode.") + var xad: Bool = false + + @Flag(name: .long, help: "Enable strict XAD parsing.") + var xadStrict: Bool = false + + @Flag(name: .long, help: "Enable Paged.js hooks (HTML only).") + var xadPagedJS: Bool = false + + @Option(name: .long, help: "Path to XAD template (.adoc).") + var xadTemplate: String? + @Option( name: [.customShort("a"), .customLong("attribute")], parsing: .unconditionalSingleValue, @@ -200,6 +212,12 @@ struct HTML: AsyncParsableCommand { var extensions: [String] = [] mutating func run() async throws { + let xadOptions = makeXadOptions( + enabled: xad, + pagedJS: xadPagedJS, + strict: xadStrict, + templatePath: xadTemplate + ) try renderDocument( backend: .html5, defaultExtension: "html", @@ -208,7 +226,126 @@ struct HTML: AsyncParsableCommand { templateRoot: template, attributeAssignments: attributeAssignments, outputPath: output, - enabledExtensions: extensions + enabledExtensions: extensions, + xadOptions: xadOptions + ) + } +} + +struct XadHtml: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "xad-html", + abstract: "Render AsciiDoc to HTML with XAD enabled." + ) + + @Flag(help: "Read source from stdin. Otherwise provide a path argument.") + var stdin: Bool = false + + @Option(help: "Path to the Stencil templates root directory.") + var template: String = "Templates" + + @Flag(name: .long, help: "Enable strict XAD parsing.") + var xadStrict: Bool = false + + @Option(name: .long, help: "Path to XAD template (.adoc).") + var xadTemplate: String? + + @Option( + name: [.customShort("a"), .customLong("attribute")], + parsing: .unconditionalSingleValue, + help: "Set document attribute (name[=value]). Repeatable." + ) + var attributeAssignments: [String] = [] + + @Option(name: .shortAndLong, help: "Write rendered output to this path.") + var output: String? + + @Argument(help: "Path to the .adoc document (omit when using --stdin).") + var inputPath: String? + + @Option( + name: [.customShort("e"), .customLong("extension")], + parsing: .upToNextOption, + help: "Enable an extension by name (repeatable)." + ) + var extensions: [String] = [] + + mutating func run() async throws { + let xadOptions = makeXadOptions( + enabled: true, + pagedJS: false, + strict: xadStrict, + templatePath: xadTemplate + ) + try renderDocument( + backend: .html5, + defaultExtension: "html", + stdin: stdin, + inputPath: inputPath, + templateRoot: template, + attributeAssignments: attributeAssignments, + outputPath: output, + enabledExtensions: extensions, + xadOptions: xadOptions + ) + } +} + +struct XadPagedHtml: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "xad-paged-html", + abstract: "Render AsciiDoc to HTML with XAD + Paged.js enabled." + ) + + @Flag(help: "Read source from stdin. Otherwise provide a path argument.") + var stdin: Bool = false + + @Option(help: "Path to the Stencil templates root directory.") + var template: String = "Templates" + + @Flag(name: .long, help: "Enable strict XAD parsing.") + var xadStrict: Bool = false + + @Option(name: .long, help: "Path to XAD template (.adoc).") + var xadTemplate: String? + + @Option( + name: [.customShort("a"), .customLong("attribute")], + parsing: .unconditionalSingleValue, + help: "Set document attribute (name[=value]). Repeatable." + ) + var attributeAssignments: [String] = [] + + @Option(name: .shortAndLong, help: "Write rendered output to this path.") + var output: String? + + @Argument(help: "Path to the .adoc document (omit when using --stdin).") + var inputPath: String? + + @Option( + name: [.customShort("e"), .customLong("extension")], + parsing: .upToNextOption, + help: "Enable an extension by name (repeatable)." + ) + var extensions: [String] = [] + + mutating func run() async throws { + let xadOptions = makeXadOptions( + enabled: true, + pagedJS: true, + strict: xadStrict, + templatePath: xadTemplate + ) + try renderDocument( + backend: .html5, + defaultExtension: "html", + stdin: stdin, + inputPath: inputPath, + templateRoot: template, + attributeAssignments: attributeAssignments, + outputPath: output, + enabledExtensions: extensions, + xadOptions: xadOptions ) } } @@ -225,6 +362,18 @@ struct DocBook: AsyncParsableCommand { @Option(help: "Path to the Stencil templates root directory.") var template: String = "Templates" + @Flag(name: .long, help: "Enable XAD mode.") + var xad: Bool = false + + @Flag(name: .long, help: "Enable strict XAD parsing.") + var xadStrict: Bool = false + + @Flag(name: .long, help: "Enable Paged.js hooks (HTML only).") + var xadPagedJS: Bool = false + + @Option(name: .long, help: "Path to XAD template (.adoc).") + var xadTemplate: String? + @Option( name: [.customShort("a"), .customLong("attribute")], parsing: .unconditionalSingleValue, @@ -246,6 +395,12 @@ struct DocBook: AsyncParsableCommand { var extensions: [String] = [] mutating func run() async throws { + let xadOptions = makeXadOptions( + enabled: xad, + pagedJS: xadPagedJS, + strict: xadStrict, + templatePath: xadTemplate + ) try renderDocument( backend: .docbook5, defaultExtension: "xml", @@ -254,7 +409,8 @@ struct DocBook: AsyncParsableCommand { templateRoot: template, attributeAssignments: attributeAssignments, outputPath: output, - enabledExtensions: extensions + enabledExtensions: extensions, + xadOptions: xadOptions ) } } @@ -271,6 +427,18 @@ struct Latex: AsyncParsableCommand { @Option(help: "Path to the Stencil templates root directory.") var template: String = "Templates" + @Flag(name: .long, help: "Enable XAD mode.") + var xad: Bool = false + + @Flag(name: .long, help: "Enable strict XAD parsing.") + var xadStrict: Bool = false + + @Flag(name: .long, help: "Enable Paged.js hooks (HTML only).") + var xadPagedJS: Bool = false + + @Option(name: .long, help: "Path to XAD template (.adoc).") + var xadTemplate: String? + @Option( name: [.customShort("a"), .customLong("attribute")], parsing: .unconditionalSingleValue, @@ -292,6 +460,12 @@ struct Latex: AsyncParsableCommand { var extensions: [String] = [] mutating func run() async throws { + let xadOptions = makeXadOptions( + enabled: xad, + pagedJS: xadPagedJS, + strict: xadStrict, + templatePath: xadTemplate + ) try renderDocument( backend: .latex, defaultExtension: "tex", @@ -300,7 +474,8 @@ struct Latex: AsyncParsableCommand { templateRoot: template, attributeAssignments: attributeAssignments, outputPath: output, - enabledExtensions: extensions + enabledExtensions: extensions, + xadOptions: xadOptions ) } } @@ -536,6 +711,21 @@ struct Antora: AsyncParsableCommand { // Rendering helpers extension PlantUMLExtension.Format: ExpressibleByArgument {} +private func makeXadOptions( + enabled: Bool, + pagedJS: Bool, + strict: Bool, + templatePath: String? +) -> XADOptions { + let shouldEnable = enabled || pagedJS || templatePath != nil + return XADOptions( + enabled: shouldEnable, + strict: strict, + pagedJS: pagedJS, + templatePath: templatePath + ) +} + private func renderDocument( backend: Backend, defaultExtension: String, @@ -544,7 +734,8 @@ private func renderDocument( templateRoot: String, attributeAssignments: [String], outputPath: String?, - enabledExtensions: [String] + enabledExtensions: [String], + xadOptions: XADOptions ) throws { let (source, sourcePath) = try readRenderSource(stdin: stdin, inputPath: inputPath) let documentDirectory = sourcePath.map { URL(fileURLWithPath: $0).deletingLastPathComponent() } @@ -581,7 +772,8 @@ private func renderDocument( text: preprocessedSource, attributes: parserAttributes, lockedAttributeNames: seed.locked, - preprocessorOptions: preprocessorOptions + preprocessorOptions: preprocessorOptions, + xadOptions: xadOptions ) doc = extensionHost.runDidParse(document: doc) @@ -589,7 +781,7 @@ private func renderDocument( let engine = StencilTemplateEngine(templateRoot: templateRoot) let renderer = DocumentRenderer( engine: engine, - config: RenderConfig(backend: backend) + config: RenderConfig(backend: backend, xadOptions: xadOptions) ) let rendered = try renderer.render(document: doc) try writeRenderedOutput(rendered, explicitPath: outputPath, sourcePath: sourcePath, defaultExtension: defaultExtension) diff --git a/Sources/asciidoc-swift/asciidoc-swift.docc/asciidoc-swift.md b/Sources/asciidoc-swift/asciidoc-swift.docc/asciidoc-swift.md index 790955c..cc1a464 100644 --- a/Sources/asciidoc-swift/asciidoc-swift.docc/asciidoc-swift.md +++ b/Sources/asciidoc-swift/asciidoc-swift.docc/asciidoc-swift.md @@ -53,6 +53,59 @@ asciidoc-swift html [--stdin] [--template