From d3b5c472e4c4396a3e4f937faa2803b6d6212291 Mon Sep 17 00:00:00 2001 From: Koutaro Mukai Date: Fri, 6 Feb 2026 09:48:36 +0900 Subject: [PATCH 1/4] feat: implement htmlProcessor/xhtmlProcessor for HTML document transformation --- docs/api-javascript.md | 67 ++++++-- docs/config.md | 24 +++ package.json | 11 +- pnpm-lock.yaml | 280 ++++++++++++++------------------ src/config/resolve.ts | 49 +++++- src/config/schema.ts | 30 ++++ src/index.ts | 6 + src/processor/compile.ts | 73 ++++++--- src/processor/html-processor.ts | 180 ++++++++++++++++++++ src/processor/html.tsx | 3 + tests/config.test.ts | 22 +++ 11 files changed, 545 insertions(+), 200 deletions(-) create mode 100644 src/processor/html-processor.ts diff --git a/docs/api-javascript.md b/docs/api-javascript.md index d2052c74..47f62cea 100644 --- a/docs/api-javascript.md +++ b/docs/api-javascript.md @@ -14,11 +14,13 @@ ### Interfaces +- [`HtmlOptions`](#htmloptions) - [`StringifyMarkdownOptions`](#stringifymarkdownoptions) - [`TemplateVariable`](#templatevariable) ### Type Aliases +- [`HtmlProcessorFactory`](#htmlprocessorfactory) - [`Metadata`](#metadata) - [`StructuredDocument`](#structureddocument) - [`StructuredDocumentSection`](#structureddocumentsection) @@ -28,6 +30,8 @@ ### Variables +- [`defaultHtmlProcessor`](#defaulthtmlprocessor) +- [`defaultXhtmlProcessor`](#defaultxhtmlprocessor) - [`readMetadata`](#readmetadata) ## Functions @@ -140,7 +144,7 @@ build({ ###### logLevel? -`"info"` \| `"silent"` \| `"verbose"` \| `"debug"` = `...` +`"verbose"` \| `"info"` \| `"silent"` \| `"debug"` = `...` ###### openViewer? @@ -372,7 +376,7 @@ Scaffold a new Vivliostyle project. ###### logLevel? -`"info"` \| `"silent"` \| `"verbose"` \| `"debug"` = `...` +`"verbose"` \| `"info"` \| `"silent"` \| `"debug"` = `...` ###### openViewer? @@ -602,7 +606,7 @@ Scaffold a new Vivliostyle project. ###### logLevel? -`"info"` \| `"silent"` \| `"verbose"` \| `"debug"` = `...` +`"verbose"` \| `"info"` \| `"silent"` \| `"debug"` = `...` ###### openViewer? @@ -852,7 +856,7 @@ Open a browser for previewing the publication. ###### logLevel? -`"info"` \| `"silent"` \| `"verbose"` \| `"debug"` = `...` +`"verbose"` \| `"info"` \| `"silent"` \| `"debug"` = `...` ###### openViewer? @@ -1010,6 +1014,19 @@ Unified processor. ## Interfaces +### HtmlOptions + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `contentType?` | `"text/html"` \| `"application/xhtml+xml"` | Content type: 'text/html' or 'application/xhtml+xml' | +| `language?` | `string` | Document language (sets html lang if not present) | +| `style?` | `string`[] | Paths to stylesheets to inject | +| `title?` | `string` | Document title (sets if not present) | + +*** + ### StringifyMarkdownOptions Option for convert Markdown to a stringify (HTML). @@ -1022,12 +1039,12 @@ Option for convert Markdown to a stringify (HTML). | <a id="disableformathtml"></a> `disableFormatHtml?` | `boolean` | Disable automatic HTML format. | | <a id="hardlinebreaks"></a> `hardLineBreaks?` | `boolean` | Add `<br>` at the position of hard line breaks, without needing spaces. | | <a id="imgfigcaptionorder"></a> `imgFigcaptionOrder?` | `"img-figcaption"` \| `"figcaption-img"` | Order of img and figcaption elements in figure. | -| <a id="language"></a> `language?` | `string` | Document language (ignored in partial mode). | +| <a id="language-1"></a> `language?` | `string` | Document language (ignored in partial mode). | | <a id="math"></a> `math?` | `boolean` | Enable math syntax. | | <a id="partial"></a> `partial?` | `boolean` | Output markdown fragments. | | <a id="replace"></a> `replace?` | `ReplaceRule`[] | Replacement handler for HTML string. | -| <a id="style"></a> `style?` | `string` \| `string`[] | Custom stylesheet path/URL. | -| <a id="title"></a> `title?` | `string` | Document title (ignored in partial mode). | +| <a id="style-1"></a> `style?` | `string` \| `string`[] | Custom stylesheet path/URL. | +| <a id="title-1"></a> `title?` | `string` | Document title (ignored in partial mode). | *** @@ -1067,9 +1084,9 @@ Option for convert Markdown to a stringify (HTML). | `input.entry` | `string` | | `input.format` | `InputFormat` | | <a id="installdependencies"></a> `installDependencies?` | `boolean` | -| <a id="language-1"></a> `language` | `string` | +| <a id="language-2"></a> `language` | `string` | | <a id="logger"></a> `logger?` | `LoggerInterface` | -| <a id="loglevel"></a> `logLevel?` | `"info"` \| `"silent"` \| `"verbose"` \| `"debug"` | +| <a id="loglevel"></a> `logLevel?` | `"verbose"` \| `"info"` \| `"silent"` \| `"debug"` | | <a id="openviewer"></a> `openViewer?` | `boolean` | | <a id="output"></a> `output?` | `object` & `object` & `object`[] | | <a id="port"></a> `port?` | `number` | @@ -1091,12 +1108,12 @@ Option for convert Markdown to a stringify (HTML). | <a id="stderr"></a> `stderr?` | `Writable` | | <a id="stdin"></a> `stdin?` | `Readable` | | <a id="stdout"></a> `stdout?` | `Writable` | -| <a id="style-1"></a> `style?` | `string` | +| <a id="style-2"></a> `style?` | `string` | | <a id="template"></a> `template?` | `string` | | <a id="theme"></a> `theme?` | `string` \| `object` & `object` \| (`string` \| `object` & `object`)[] | | <a id="themepackage"></a> `themePackage?` | `VivliostylePackageJson` | | <a id="timeout"></a> `timeout?` | `number` | -| <a id="title-1"></a> `title` | `string` | +| <a id="title-2"></a> `title` | `string` | | <a id="userstyle"></a> `userStyle?` | `string` | | <a id="viewer"></a> `viewer?` | `string` | | <a id="viewerparam"></a> `viewerParam?` | `string` | @@ -1105,6 +1122,22 @@ Option for convert Markdown to a stringify (HTML). ## Type Aliases +### HtmlProcessorFactory() + +> **HtmlProcessorFactory** = (`options`) => `unified.Processor` + +#### Parameters + +##### options + +[`HtmlOptions`](#htmloptions) + +#### Returns + +`unified.Processor` + +*** + ### Metadata > **Metadata** = `object` @@ -1298,6 +1331,18 @@ https://github.com/vivliostyle/vivliostyle-cli/blob/main/docs/config.md ## Variables +### defaultHtmlProcessor + +> `const` **defaultHtmlProcessor**: [`HtmlProcessorFactory`](#htmlprocessorfactory) + +*** + +### defaultXhtmlProcessor + +> `const` **defaultXhtmlProcessor**: [`HtmlProcessorFactory`](#htmlprocessorfactory) + +*** + ### readMetadata() > `const` **readMetadata**: (`md`, `customKeys?`) => [`Metadata`](#metadata) diff --git a/docs/config.md b/docs/config.md index aa2b5ce2..57f82c43 100644 --- a/docs/config.md +++ b/docs/config.md @@ -99,6 +99,12 @@ type VivliostyleConfigSchema = - `documentMetadataReader`: (content: string) => import("@vivliostyle/vfm").Metadata Custom function to extract metadata from the source document content. + - `htmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + Custom function to provide a unified Processor for transforming HTML documents. + + - `xhtmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + Custom function to provide a unified Processor for transforming XHTML documents. + - `vfm`: [VfmConfig](#vfmconfig) Options for converting Markdown into a stringified format (HTML). @@ -188,6 +194,12 @@ type BuildTask = { documentMetadataReader?: ( content: string, ) => import("@vivliostyle/vfm").Metadata; + htmlProcessor?: ( + options: import("../processor/html-processor.js").HtmlOptions, + ) => import("unified").Processor; + xhtmlProcessor?: ( + options: import("../processor/html-processor.js").HtmlOptions, + ) => import("unified").Processor; vfm?: VfmConfig; image?: string; http?: boolean; @@ -344,6 +356,12 @@ type CoverEntryConfig = { - `documentMetadataReader`: (content: string) => import("@vivliostyle/vfm").Metadata Custom function to extract metadata from the source document content. + - `htmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + Custom function to provide a unified Processor for transforming HTML documents. + + - `xhtmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + Custom function to provide a unified Processor for transforming XHTML documents. + #### Type definition ```ts @@ -364,6 +382,12 @@ type ArticleEntryConfig = { documentMetadataReader?: ( content: string, ) => import("@vivliostyle/vfm").Metadata; + htmlProcessor?: ( + options: import("../processor/html-processor.js").HtmlOptions, + ) => import("unified").Processor; + xhtmlProcessor?: ( + options: import("../processor/html-processor.js").HtmlOptions, + ) => import("unified").Processor; }; ``` diff --git a/package.json b/package.json index d36bca69..9a788888 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "handlebars": "4.7.8", "hast-util-to-html": "9.0.5", "hastscript": "9.0.1", + "hastscript-v7": "npm:hastscript@^7.2.0", "lcid": "5.0.0", "mime-types": "3.0.1", "mupdf": "1.26.4", @@ -61,6 +62,7 @@ "picomatch": "4.0.2", "press-ready": "4.0.3", "puppeteer-core": "24.27.0", + "rehype": "11.0.0", "resolve-pkg": "2.0.0", "semver": "7.7.1", "sirv": "3.0.1", @@ -70,6 +72,9 @@ "tinyglobby": "0.2.13", "title-case": "4.3.2", "tmp": "0.2.4", + "unist-builder": "^3.0.1", + "unist-util-visit": "4.1.2", + "unified": "9.2.2", "upath": "2.0.1", "uuid": "11.1.0", "valibot": "1.2.0", @@ -77,6 +82,9 @@ "vite": "^6.3.5", "w3c-xmlserializer": "5.0.0", "whatwg-mimetype": "4.0.0", + "xast-util-from-xml": "^3.0.0", + "xast-util-to-xml": "^3.0.2", + "xastscript": "3.1.1", "yocto-spinner": "0.2.2", "yoctocolors": "2.1.1" }, @@ -91,7 +99,9 @@ "@types/dompurify": "3.2.0", "@types/fs-extra": "11.0.4", "@types/hast": "3.0.4", + "@types/hast-v2": "npm:@types/hast@^2.0.0", "@types/jsdom": "21.1.7", + "@types/xast": "^1.0.0", "@types/mime-types": "2.1.4", "@types/node": "22.15.2", "@types/npm-package-arg": "6.1.4", @@ -119,7 +129,6 @@ "typedoc": "^0.28.3", "typedoc-plugin-markdown": "4.6.3", "typescript": "5.8.3", - "unified": "9.2.2", "vitest": "3.1.2" }, "main": "dist/index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdbae87e..98772a84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: hastscript: specifier: 9.0.1 version: 9.0.1 + hastscript-v7: + specifier: npm:hastscript@^7.2.0 + version: hastscript@7.2.0 lcid: specifier: 5.0.0 version: 5.0.0 @@ -128,6 +131,9 @@ importers: puppeteer-core: specifier: 24.27.0 version: 24.27.0 + rehype: + specifier: 11.0.0 + version: 11.0.0 resolve-pkg: specifier: 2.0.0 version: 2.0.0 @@ -155,6 +161,15 @@ importers: tmp: specifier: 0.2.4 version: 0.2.4 + unified: + specifier: 9.2.2 + version: 9.2.2 + unist-builder: + specifier: ^3.0.1 + version: 3.0.1 + unist-util-visit: + specifier: 4.1.2 + version: 4.1.2 upath: specifier: 2.0.1 version: 2.0.1 @@ -176,6 +191,15 @@ importers: whatwg-mimetype: specifier: 4.0.0 version: 4.0.0 + xast-util-from-xml: + specifier: ^3.0.0 + version: 3.0.0 + xast-util-to-xml: + specifier: ^3.0.2 + version: 3.0.2 + xastscript: + specifier: 3.1.1 + version: 3.1.1 yocto-spinner: specifier: 0.2.2 version: 0.2.2 @@ -213,6 +237,9 @@ importers: '@types/hast': specifier: 3.0.4 version: 3.0.4 + '@types/hast-v2': + specifier: npm:@types/hast@^2.0.0 + version: '@types/hast@2.3.10' '@types/jsdom': specifier: 21.1.7 version: 21.1.7 @@ -249,6 +276,9 @@ importers: '@types/whatwg-mimetype': specifier: 3.0.2 version: 3.0.2 + '@types/xast': + specifier: ^1.0.0 + version: 1.0.7 '@vitest/coverage-v8': specifier: 3.1.2 version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(tsx@4.19.3)(yaml@2.8.1)) @@ -297,9 +327,6 @@ importers: typescript: specifier: 5.8.3 version: 5.8.3 - unified: - specifier: 9.2.2 - version: 9.2.2 vitest: specifier: 3.1.2 version: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(tsx@4.19.3)(yaml@2.8.1) @@ -340,8 +367,8 @@ importers: specifier: workspace:* version: link:../.. rehype-expressive-code: - specifier: ^0.37.0 - version: 0.37.1 + specifier: 0.41.6 + version: 0.41.6 rehype-stringify: specifier: ^8.0.0 version: 8.0.0 @@ -349,14 +376,17 @@ importers: specifier: ^9.0.0 version: 9.0.0 remark-rehype: - specifier: ^9.0.0 - version: 9.1.0 + specifier: ^8.1.0 + version: 8.1.0 remark-ruby: specifier: ^0.4.0 version: 0.4.0(remark-parse@9.0.0) unified: specifier: ^9.2.0 version: 9.2.2 + unist-util-visit: + specifier: ^4.1.0 + version: 4.1.2 examples/local-theme: dependencies: @@ -1196,17 +1226,17 @@ packages: cpu: [x64] os: [win32] - '@expressive-code/core@0.37.1': - resolution: {integrity: sha512-nYgsK3uxK4W46v0IolHdB5+T6MQfy5weTpyB3kbzr11ur2UKUy2oHGhbWa+hRThfYEF1PM+TFxMtWU7amcjF8A==} + '@expressive-code/core@0.41.6': + resolution: {integrity: sha512-FvJQP+hG0jWi/FLBSmvHInDqWR7jNANp9PUDjdMqSshHb0y7sxx3vHuoOr6SgXjWw+MGLqorZyPQ0aAlHEok6g==} - '@expressive-code/plugin-frames@0.37.1': - resolution: {integrity: sha512-U1Pk8qmSlGaIvNpmo42pxVS+ROVkPUPtAgVR5OU9261zKw6r/MN7b5dCDE5B6QBqRi7iOS7/DQ/zYTzHcIQJQw==} + '@expressive-code/plugin-frames@0.41.6': + resolution: {integrity: sha512-d+hkSYXIQot6fmYnOmWAM+7TNWRv/dhfjMsNq+mIZz8Tb4mPHOcgcfZeEM5dV9TDL0ioQNvtcqQNuzA1sRPjxg==} - '@expressive-code/plugin-shiki@0.37.1': - resolution: {integrity: sha512-zzrX0kn6JMxm+0fKWpP1W8F3bLK8PYIUDvM5UUEplGjL5Z0otBqycHWalucl0Mk9aKX1UThXaUTuCQEVOkNSxg==} + '@expressive-code/plugin-shiki@0.41.6': + resolution: {integrity: sha512-Y6zmKBmsIUtWTzdefqlzm/h9Zz0Rc4gNdt2GTIH7fhHH2I9+lDYCa27BDwuBhjqcos6uK81Aca9dLUC4wzN+ng==} - '@expressive-code/plugin-text-markers@0.37.1': - resolution: {integrity: sha512-AjZG/SyE41uWcpAae0w9DVtSii/YkdhOsjl6godLsI8vRGBdUdYvgoCebjb33gySa7g+9NcWxGp3oEKCBut1wA==} + '@expressive-code/plugin-text-markers@0.41.6': + resolution: {integrity: sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q==} '@gerrit0/mini-shiki@3.17.0': resolution: {integrity: sha512-Bpf6WuFar20ZXL6qU6VpVl4bVQfyyYiX+6O4xrns4nkU3Mr8paeupDbS1HENpcLOYj7pN4Rkd/yCaPA0vQwKww==} @@ -1809,48 +1839,30 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@1.29.2': - resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} - '@shikijs/core@3.15.0': resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==} - '@shikijs/engine-javascript@1.29.2': - resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==} - '@shikijs/engine-javascript@3.15.0': resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} - '@shikijs/engine-oniguruma@1.29.2': - resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} - '@shikijs/engine-oniguruma@3.15.0': resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} '@shikijs/engine-oniguruma@3.17.1': resolution: {integrity: sha512-fsXPy4va/4iblEGS+22nP5V08IwwBcM+8xHUzSON0QmHm29/AJRghA95w9VDnxuwp9wOdJxEhfPkKp6vqcsN+w==} - '@shikijs/langs@1.29.2': - resolution: {integrity: sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==} - '@shikijs/langs@3.15.0': resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} '@shikijs/langs@3.17.1': resolution: {integrity: sha512-YTBVN+L2j7zBuOVjNZ2XiSNQEkm/7wZ1TSc5UO77GJPcg7Rk25WSscWA7y8pW7Bo25JIU0EWchUkq/UQjOJlJA==} - '@shikijs/themes@1.29.2': - resolution: {integrity: sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==} - '@shikijs/themes@3.15.0': resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} '@shikijs/themes@3.17.1': resolution: {integrity: sha512-aohwwqNUB5h2ATfgrqYRPl8vyazqCiQ2wIV4xq+UzaBRHpqLMGSemkasK+vIEpl0YaendoaKUsDfpwhCqyHIaQ==} - '@shikijs/types@1.29.2': - resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==} - '@shikijs/types@3.15.0': resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} @@ -1966,9 +1978,6 @@ packages: '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - '@types/hast@2.3.4': - resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -2113,6 +2122,9 @@ packages: '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/xast@1.0.7': + resolution: {integrity: sha512-QkazU2uUKr+9u7EsMfDoNQ9UtWB6sfOIGbNevJme8qQMeAHEnHmCfbsBJf2eXUm+07kFQfUKbCQp/Y6/8O25kw==} + '@types/yargs-parser@15.0.0': resolution: {integrity: sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==} @@ -2944,9 +2956,6 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - emoji-regex-xs@1.0.0: - resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} - emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} @@ -3141,8 +3150,8 @@ packages: exponential-backoff@3.1.2: resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} - expressive-code@0.37.1: - resolution: {integrity: sha512-y7+5K2uJOt+Hy0KEPm83t974flu0O/DstvmezoiWXwV3Cwqf622CHMlWssaZ8uqFrJaJ1ES1cMcYXJtZSxe+9A==} + expressive-code@0.41.6: + resolution: {integrity: sha512-W/5+IQbrpCIM5KGLjO35wlp1NCwDOOVQb+PAvzEoGkW1xjGM807ZGfBKptNWH6UECvt6qgmLyWolCMYKh7eQmA==} extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} @@ -4527,9 +4536,6 @@ packages: oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - oniguruma-to-es@2.3.0: - resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} - oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} @@ -4963,23 +4969,17 @@ packages: refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} - regex-recursion@5.1.1: - resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} - regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@5.1.1: - resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} - regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} - rehype-expressive-code@0.37.1: - resolution: {integrity: sha512-HK6RJubSfgC6jpfk7UpGQcEgpMP+dhKrUQ1MmnDFYT9mOHteoLJv09WMURpQrIvXDpKCiAsaWQUBfNqRxjfL2Q==} + rehype-expressive-code@0.41.6: + resolution: {integrity: sha512-aBMX8kxPtjmDSFUdZlAWJkMvsQ4ZMASfee90JWIAV8tweltXLzkWC3q++43ToTelI8ac5iC0B3/S/Cl4Ql1y2g==} rehype-format@3.1.0: resolution: {integrity: sha512-XC88CLU83x6ocwHbDsqbK/y+qNxqWcSp7gZdYeJzUZPQH05ABFke3Zb+H53UGsRAUTB2W95/UMMtn/pM+UFiKQ==} @@ -4987,6 +4987,9 @@ packages: rehype-minify-whitespace@4.0.5: resolution: {integrity: sha512-QC3Z+bZ5wbv+jGYQewpAAYhXhzuH/TVRx7z08rurBmh9AbG8Nu8oJnvs9LWj43Fd/C7UIhXoQ7Wddgt+ThWK5g==} + rehype-parse@7.0.1: + resolution: {integrity: sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==} + rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} @@ -5005,6 +5008,9 @@ packages: rehype-stringify@8.0.0: resolution: {integrity: sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g==} + rehype@11.0.0: + resolution: {integrity: sha512-qXqRqiCFJD5CJ61CSJuNImTFrm3zVkOU9XywHDwrUuvWN74MWt72KJ67c5CM5x8g0vGcOkRVCrYj85vqkmHulQ==} + rehype@13.0.2: resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} @@ -5044,9 +5050,6 @@ packages: remark-rehype@8.1.0: resolution: {integrity: sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==} - remark-rehype@9.1.0: - resolution: {integrity: sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q==} - remark-ruby@0.4.0: resolution: {integrity: sha512-4GUhQuuIlKzP6kX1Ty89yvje6Rzs4BuVSv98L0Npx0784I9MpZejUbP2uesP/3eNsNtnGOVJmi3oV5IysNWtuA==} peerDependencies: @@ -5223,9 +5226,6 @@ packages: engines: {node: '>=18'} hasBin: true - shiki@1.29.2: - resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} - shiki@3.15.0: resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} @@ -5750,9 +5750,6 @@ packages: unicode-trie@2.0.0: resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - unified@10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -6231,6 +6228,15 @@ packages: utf-8-validate: optional: true + xast-util-from-xml@3.0.0: + resolution: {integrity: sha512-zEfGxnue7vHbayZnRBAjJsjV1dQG6XWKLAzrsPxX2l4WHmqGZxM76XxRblaTHvcOYMFLlnVp3DFcUAl+Qk9SNQ==} + + xast-util-to-xml@3.0.2: + resolution: {integrity: sha512-lbnJEVui7Lk9W/TydInkin5OnOyTAr2b4OpK5YjXo94qqoXaR6mFE/evXfOrBev9EnD91r0FOPxrLfTT0qLITQ==} + + xastscript@3.1.1: + resolution: {integrity: sha512-rE7oVPLENyIZNKeRkXE96ao/QiucenYROqdBnEuNNkQHlyxf3vJ5EMNh2QQ4AYhdf9v/dVuxJ7F4u4AaOOhkOA==} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -7043,7 +7049,7 @@ snapshots: '@esbuild/win32-x64@0.25.3': optional: true - '@expressive-code/core@0.37.1': + '@expressive-code/core@0.41.6': dependencies: '@ctrl/tinycolor': 4.1.0 hast-util-select: 6.0.4 @@ -7053,20 +7059,20 @@ snapshots: postcss: 8.5.3 postcss-nested: 6.2.0(postcss@8.5.3) unist-util-visit: 5.0.0 - unist-util-visit-parents: 6.0.1 + unist-util-visit-parents: 6.0.2 - '@expressive-code/plugin-frames@0.37.1': + '@expressive-code/plugin-frames@0.41.6': dependencies: - '@expressive-code/core': 0.37.1 + '@expressive-code/core': 0.41.6 - '@expressive-code/plugin-shiki@0.37.1': + '@expressive-code/plugin-shiki@0.41.6': dependencies: - '@expressive-code/core': 0.37.1 - shiki: 1.29.2 + '@expressive-code/core': 0.41.6 + shiki: 3.15.0 - '@expressive-code/plugin-text-markers@0.37.1': + '@expressive-code/plugin-text-markers@0.41.6': dependencies: - '@expressive-code/core': 0.37.1 + '@expressive-code/core': 0.41.6 '@gerrit0/mini-shiki@3.17.0': dependencies: @@ -7647,15 +7653,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.0': optional: true - '@shikijs/core@1.29.2': - dependencies: - '@shikijs/engine-javascript': 1.29.2 - '@shikijs/engine-oniguruma': 1.29.2 - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - '@shikijs/core@3.15.0': dependencies: '@shikijs/types': 3.15.0 @@ -7663,23 +7660,12 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 2.3.0 - '@shikijs/engine-javascript@3.15.0': dependencies: '@shikijs/types': 3.15.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.3 - '@shikijs/engine-oniguruma@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@3.15.0': dependencies: '@shikijs/types': 3.15.0 @@ -7690,10 +7676,6 @@ snapshots: '@shikijs/types': 3.17.1 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/langs@3.15.0': dependencies: '@shikijs/types': 3.15.0 @@ -7702,10 +7684,6 @@ snapshots: dependencies: '@shikijs/types': 3.17.1 - '@shikijs/themes@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/themes@3.15.0': dependencies: '@shikijs/types': 3.15.0 @@ -7714,11 +7692,6 @@ snapshots: dependencies: '@shikijs/types': 3.17.1 - '@shikijs/types@1.29.2': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/types@3.15.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -7858,13 +7831,9 @@ snapshots: dependencies: '@types/unist': 2.0.11 - '@types/hast@2.3.4': - dependencies: - '@types/unist': 3.0.3 - '@types/hast@3.0.4': dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 '@types/jsdom@21.1.7': dependencies: @@ -8018,6 +7987,10 @@ snapshots: '@types/whatwg-mimetype@3.0.2': {} + '@types/xast@1.0.7': + dependencies: + '@types/unist': 2.0.11 + '@types/yargs-parser@15.0.0': {} '@types/yargs@15.0.12': @@ -8945,8 +8918,6 @@ snapshots: ee-first@1.1.1: {} - emoji-regex-xs@1.0.0: {} - emoji-regex@10.5.0: {} emoji-regex@8.0.0: {} @@ -9201,12 +9172,12 @@ snapshots: exponential-backoff@3.1.2: {} - expressive-code@0.37.1: + expressive-code@0.41.6: dependencies: - '@expressive-code/core': 0.37.1 - '@expressive-code/plugin-frames': 0.37.1 - '@expressive-code/plugin-shiki': 0.37.1 - '@expressive-code/plugin-text-markers': 0.37.1 + '@expressive-code/core': 0.41.6 + '@expressive-code/plugin-frames': 0.41.6 + '@expressive-code/plugin-shiki': 0.41.6 + '@expressive-code/plugin-text-markers': 0.41.6 extend-shallow@2.0.1: dependencies: @@ -9842,7 +9813,7 @@ snapshots: '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 - property-information: 7.0.0 + property-information: 7.1.0 space-separated-tokens: 2.0.2 hexoid@2.0.0: {} @@ -10296,7 +10267,7 @@ snapshots: '@types/mdast': 4.0.3 escape-string-regexp: 5.0.0 unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.1 + unist-util-visit-parents: 6.0.2 mdast-util-from-markdown@0.8.5: dependencies: @@ -11079,12 +11050,6 @@ snapshots: oniguruma-parser@0.12.1: {} - oniguruma-to-es@2.3.0: - dependencies: - emoji-regex-xs: 1.0.0 - regex: 5.1.1 - regex-recursion: 5.1.1 - oniguruma-to-es@4.3.3: dependencies: oniguruma-parser: 0.12.1 @@ -11561,28 +11526,19 @@ snapshots: parse-entities: 2.0.0 prismjs: 1.27.0 - regex-recursion@5.1.1: - dependencies: - regex: 5.1.1 - regex-utilities: 2.3.0 - regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 regex-utilities@2.3.0: {} - regex@5.1.1: - dependencies: - regex-utilities: 2.3.0 - regex@6.0.1: dependencies: regex-utilities: 2.3.0 - rehype-expressive-code@0.37.1: + rehype-expressive-code@0.41.6: dependencies: - expressive-code: 0.37.1 + expressive-code: 0.41.6 rehype-format@3.1.0: dependencies: @@ -11602,6 +11558,11 @@ snapshots: hast-util-whitespace: 1.0.4 unist-util-is: 4.1.0 + rehype-parse@7.0.1: + dependencies: + hast-util-from-parse5: 6.0.1 + parse5: 6.0.1 + rehype-parse@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -11636,6 +11597,12 @@ snapshots: dependencies: hast-util-to-html: 7.1.3 + rehype@11.0.0: + dependencies: + rehype-parse: 7.0.1 + rehype-stringify: 8.0.0 + unified: 9.2.2 + rehype@13.0.2: dependencies: '@types/hast': 3.0.4 @@ -11724,13 +11691,6 @@ snapshots: dependencies: mdast-util-to-hast: 10.2.0 - remark-rehype@9.1.0: - dependencies: - '@types/hast': 2.3.4 - '@types/mdast': 3.0.10 - mdast-util-to-hast: 11.3.0 - unified: 10.1.2 - remark-ruby@0.4.0(remark-parse@9.0.0): dependencies: browser-assert: 1.2.1 @@ -11978,17 +11938,6 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 - shiki@1.29.2: - dependencies: - '@shikijs/core': 1.29.2 - '@shikijs/engine-javascript': 1.29.2 - '@shikijs/engine-oniguruma': 1.29.2 - '@shikijs/langs': 1.29.2 - '@shikijs/themes': 1.29.2 - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - shiki@3.15.0: dependencies: '@shikijs/core': 3.15.0 @@ -12543,16 +12492,6 @@ snapshots: pako: 0.2.9 tiny-inflate: 1.0.3 - unified@10.1.2: - dependencies: - '@types/unist': 2.0.11 - bail: 2.0.2 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 5.3.7 - unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -13013,6 +12952,23 @@ snapshots: ws@8.18.3: {} + xast-util-from-xml@3.0.0: + dependencies: + '@rgrove/parse-xml': 4.2.0 + '@types/xast': 1.0.7 + vfile-location: 4.1.0 + vfile-message: 3.1.4 + + xast-util-to-xml@3.0.2: + dependencies: + '@types/xast': 1.0.7 + ccount: 2.0.1 + stringify-entities: 4.0.4 + + xastscript@3.1.1: + dependencies: + '@types/xast': 1.0.7 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} diff --git a/src/config/resolve.ts b/src/config/resolve.ts index f22629fe..bdd7ffed 100644 --- a/src/config/resolve.ts +++ b/src/config/resolve.ts @@ -4,6 +4,11 @@ import { type StringifyMarkdownOptions, VFM, } from '@vivliostyle/vfm'; +import { + defaultHtmlProcessor, + defaultXhtmlProcessor, + type HtmlProcessorFactory, +} from '../processor/html-processor.js'; import { lookup as mime } from 'mime-types'; import fs from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -88,12 +93,15 @@ export interface FileEntrySource { pathname: string; contentType: ManuscriptMediaType; documentProcessor?: DocumentProcessor; + htmlProcessor?: HtmlProcessorFactory; } export interface UriEntrySource { type: 'uri'; href: string; rootDir: string; + htmlProcessor?: HtmlProcessorFactory; + xhtmlProcessor?: HtmlProcessorFactory; } export const manuscriptMediaTypes = [ @@ -1216,6 +1224,11 @@ function resolveComposedProjectConfig({ metadataReader: config.documentMetadataReader ?? readMetadata, } satisfies DocumentProcessor; + const rootHtmlProcessor: HtmlProcessorFactory | undefined = + config.htmlProcessor; + const rootXhtmlProcessor: HtmlProcessorFactory | undefined = + config.xhtmlProcessor; + const isContentsEntry = (entry: EntryConfig): entry is ContentsEntryConfig => entry.rel === 'contents'; const isCoverEntry = (entry: EntryConfig): entry is CoverEntryConfig => @@ -1230,16 +1243,36 @@ function resolveComposedProjectConfig({ | (FileEntrySource & { metadata: ReturnType<typeof parseFileMetadata> }) | (UriEntrySource & { metadata?: undefined }) => { if (/^https?:/.test(entryPath)) { + const htmlProcessor = + ('htmlProcessor' in entry && entry.htmlProcessor) || + rootHtmlProcessor || + defaultHtmlProcessor; + const xhtmlProcessor = + ('xhtmlProcessor' in entry && entry.xhtmlProcessor) || + rootXhtmlProcessor || + defaultXhtmlProcessor; return { type: 'uri', href: entryPath, rootDir: upath.join(workspaceDir, new URL(entryPath).host), + htmlProcessor, + xhtmlProcessor, }; } else if (entryPath.startsWith('/')) { + const htmlProcessor = + ('htmlProcessor' in entry && entry.htmlProcessor) || + rootHtmlProcessor || + defaultHtmlProcessor; + const xhtmlProcessor = + ('xhtmlProcessor' in entry && entry.xhtmlProcessor) || + rootXhtmlProcessor || + defaultXhtmlProcessor; return { type: 'uri', href: entryPath, rootDir: upath.join(workspaceDir, 'localhost'), + htmlProcessor, + xhtmlProcessor, }; } const pathname = upath.resolve(entryContextDir, entryPath); @@ -1260,7 +1293,10 @@ function resolveComposedProjectConfig({ documentProcessor.metadataReader !== readMetadata ); const contentType = - hasCustomProcessor && rawContentType !== 'text/markdown' + hasCustomProcessor && + rawContentType !== 'text/markdown' && + rawContentType !== 'text/html' && + rawContentType !== 'application/xhtml+xml' ? 'text/x-vivliostyle-custom' : rawContentType; if ( @@ -1275,6 +1311,16 @@ function resolveComposedProjectConfig({ const useDocumentProcessor = contentType === 'text/markdown' || contentType === 'text/x-vivliostyle-custom'; + const htmlProcessor = + contentType === 'text/html' + ? ('htmlProcessor' in entry && entry.htmlProcessor) || + rootHtmlProcessor || + defaultHtmlProcessor + : contentType === 'application/xhtml+xml' + ? ('xhtmlProcessor' in entry && entry.xhtmlProcessor) || + rootXhtmlProcessor || + defaultXhtmlProcessor + : undefined; return { type: 'file', pathname, @@ -1289,6 +1335,7 @@ function resolveComposedProjectConfig({ : undefined, }), ...(useDocumentProcessor && { documentProcessor }), + ...(htmlProcessor && { htmlProcessor }), }; }; diff --git a/src/config/schema.ts b/src/config/schema.ts index 11eefaca..badfab07 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -92,6 +92,32 @@ export const DocumentMetadataReaderSchema = v.pipe( `), ); +export const HtmlProcessorSchema = v.pipe( + v.function() as v.GenericSchema< + (options: import('../processor/html-processor.js').HtmlOptions) => Processor + >, + v.metadata({ + typeString: + '(options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor', + }), + v.description($` + Custom function to provide a unified Processor for transforming HTML documents. + `), +); + +export const XhtmlProcessorSchema = v.pipe( + v.function() as v.GenericSchema< + (options: import('../processor/html-processor.js').HtmlOptions) => Processor + >, + v.metadata({ + typeString: + '(options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor', + }), + v.description($` + Custom function to provide a unified Processor for transforming XHTML documents. + `), +); + export const ThemeConfig = v.pipe( v.intersect([ v.required( @@ -154,6 +180,8 @@ export const ArticleEntryConfig = v.pipe( ), documentProcessor: v.optional(DocumentProcessorSchema), documentMetadataReader: v.optional(DocumentMetadataReaderSchema), + htmlProcessor: v.optional(HtmlProcessorSchema), + xhtmlProcessor: v.optional(XhtmlProcessorSchema), }), ['path'], 'Missing required field: path', @@ -885,6 +913,8 @@ export const BuildTask = v.pipe( ), documentProcessor: DocumentProcessorSchema, documentMetadataReader: DocumentMetadataReaderSchema, + htmlProcessor: HtmlProcessorSchema, + xhtmlProcessor: XhtmlProcessorSchema, vfm: v.pipe( v.union([VfmConfig]), v.description($` diff --git a/src/index.ts b/src/index.ts index 6bd85b6c..82310af2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,12 @@ export type { VivliostyleConfigSchema, VivliostylePackageMetadata, } from './config/schema.js'; +export { + defaultHtmlProcessor, + defaultXhtmlProcessor, + type HtmlOptions, + type HtmlProcessorFactory, +} from './processor/html-processor.js'; export type { TemplateVariable } from './create-template.js'; export { createVitePlugin } from './vite-adapter.js'; /** @hidden */ diff --git a/src/processor/compile.ts b/src/processor/compile.ts index 874b34bb..84884ae5 100644 --- a/src/processor/compile.ts +++ b/src/processor/compile.ts @@ -31,10 +31,15 @@ import { getJsdomFromString, getJsdomFromUrlOrFile, processCoverHtml, - processManuscriptHtml, processTocHtml, ResourceLoader, } from './html.js'; +import { + defaultHtmlProcessor, + defaultXhtmlProcessor, + processHtml, + processHtmlString, +} from './html-processor.js'; import { processMarkdown } from './markdown.js'; import { checkThemeInstallationNecessity, @@ -203,12 +208,16 @@ export async function transformManuscript( source.contentType === 'text/html' || source.contentType === 'application/xhtml+xml' ) { - content = await getJsdomFromUrlOrFile({ src: source.pathname }); - content = await processManuscriptHtml(content, { + const processor = source.htmlProcessor ?? defaultHtmlProcessor; + const vfile = await processHtml(processor, source.pathname, { style, title: entry.title, contentType: source.contentType, - language, + language: language ?? undefined, + }); + content = getJsdomFromString({ + html: String(vfile), + contentType: source.contentType, }); } else { if (!pathEquals(source.pathname, entry.target)) { @@ -238,41 +247,55 @@ export async function transformManuscript( const contentFetcher = resourceLoader.fetcherMap.get(resourceUrl); if (contentFetcher) { const buffer = await contentFetcher; - const contentType = contentFetcher.response?.headers['content-type']; - if (!contentType || new MIMEType(contentType).essence !== 'text/html') { + const contentTypeHeader = + contentFetcher.response?.headers['content-type']; + const contentType = contentTypeHeader + ? new MIMEType(contentTypeHeader).essence + : undefined; + if ( + contentType !== 'text/html' && + contentType !== 'application/xhtml+xml' + ) { throw new Error(`The content is not an HTML document: ${resourceUrl}`); } - content = getJsdomFromString({ html: buffer.toString('utf8') }); - content = await processManuscriptHtml(content, { - style, - title: entry.title, - contentType: 'text/html', - language, + + const processor = + contentType === 'application/xhtml+xml' + ? (source.xhtmlProcessor ?? defaultXhtmlProcessor) + : (source.htmlProcessor ?? defaultHtmlProcessor); + const vfile = await processHtmlString( + processor, + buffer.toString('utf8'), + { + style, + title: entry.title, + contentType, + language: language ?? undefined, + }, + ); + content = getJsdomFromString({ + html: String(vfile), + contentType, }); } } else if (entry.rel === 'contents') { - content = getJsdomFromString({ - html: generateDefaultTocHtml({ - language, - title, - }), - }); - content = await processManuscriptHtml(content, { + const html = generateDefaultTocHtml({ language, title }); + const vfile = await processHtmlString(defaultHtmlProcessor, html, { style, title, contentType: 'text/html', - language, + language: language ?? undefined, }); + content = getJsdomFromString({ html: String(vfile) }); } else if (entry.rel === 'cover') { - content = getJsdomFromString({ - html: generateDefaultCoverHtml({ language, title: entry.title }), - }); - content = await processManuscriptHtml(content, { + const html = generateDefaultCoverHtml({ language, title: entry.title }); + const vfile = await processHtmlString(defaultHtmlProcessor, html, { style, title: entry.title, contentType: 'text/html', - language, + language: language ?? undefined, }); + content = getJsdomFromString({ html: String(vfile) }); } if (!content) { diff --git a/src/processor/html-processor.ts b/src/processor/html-processor.ts new file mode 100644 index 00000000..7f3ae5bf --- /dev/null +++ b/src/processor/html-processor.ts @@ -0,0 +1,180 @@ +import fs from 'node:fs'; + +/** + * NOTE: Vivliostyle CLI directly depends on hast@3, while VFM depends on + * hast@2 and unified@9. For consistency between documentProcessor and + * htmlProcessor, care must be taken with the libraries used in this file. + * + * unist@2 -- fixed by VFM + * `- hast@2 -- fixed by VFM + * | `- hastscript@7: https://github.com/syntax-tree/hastscript/blob/7.2.0/package.json#L60 + * `- unified@9 -- fixed by VFM + * | `- rehype@11: https://github.com/rehypejs/rehype/blob/11.0.0/packages/rehype/package.json#L23 + * `- unist-builder@3: https://github.com/syntax-tree/unist-builder/blob/3.0.1/package.json#L43 + * `- unist-util-visit@4: https://github.com/syntax-tree/unist-util-visit/blob/4.1.2/package.json#L52 + * `- xast@1: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c6ed86518513036ff29ef42c044a3de58a3cd921/types/xast/v1/package.json#L11 + * `- xast-util-from-xml@3: https://github.com/syntax-tree/xast-util-from-xml/blob/3.0.0/package.json#L42 + * `- xast-util-to-xml@3: https://github.com/syntax-tree/xast-util-to-xml/blob/3.0.2/package.json#L34 + * `- xastscript@3: https://github.com/syntax-tree/xastscript/blob/3.1.1/package.json#L48 + */ +import type * as hast from 'hast-v2'; +import { h } from 'hastscript-v7'; +import rehype from 'rehype'; +import unified from 'unified'; +import { u } from 'unist-builder'; +import { EXIT, visit } from 'unist-util-visit'; +import vfile, { type VFile } from 'vfile'; +import type * as xast from 'xast'; +import { fromXml } from 'xast-util-from-xml'; +import { toXml } from 'xast-util-to-xml'; +import { x } from 'xastscript'; + +export interface HtmlOptions { + /** Paths to stylesheets to inject */ + style?: string[]; + /** Document title (sets <title> if not present) */ + title?: string; + /** Document language (sets html lang if not present) */ + language?: string; + /** Content type: 'text/html' or 'application/xhtml+xml' */ + contentType?: 'text/html' | 'application/xhtml+xml'; +} + +export type HtmlProcessorFactory = (options: HtmlOptions) => unified.Processor; + +type AnyElement = hast.Element | xast.Element; + +function isHast(node: AnyElement): node is hast.Element { + return 'tagName' in node; +} +function getTagName(node: AnyElement): string { + return isHast(node) ? node.tagName : node.name; +} + +export const injectStyles: unified.Plugin<[Pick<HtmlOptions, 'style'>]> = + (options) => (tree) => { + const styles = options.style; + if (!styles || styles.length === 0) { + return; + } + visit(tree as hast.Root | xast.Root, 'element', (node) => { + if (getTagName(node) !== 'head') { + return; + } + styles.forEach((href) => { + const attrs = { + rel: 'stylesheet', + type: 'text/css', + href: encodeURI(href), + }; + if (isHast(node)) { + node.children.push(h('link', attrs)); + } else { + node.children.push(x('link', attrs)); + } + }); + return EXIT; + }); + }; + +export const setTitle: unified.Plugin<[Pick<HtmlOptions, 'title'>]> = + (options) => (tree) => { + const title = options.title; + if (title === undefined) { + return; + } + + // First, try to update existing title + let titleSet = false; + visit(tree as hast.Root | xast.Root, 'element', (node) => { + if (getTagName(node) !== 'title') { + return; + } + node.children = [u('text', title)]; + titleSet = true; + return EXIT; + }); + if (titleSet) { + return; + } + + // If no existing title, add one to head + visit(tree as hast.Root | xast.Root, 'element', (node) => { + if (getTagName(node) !== 'head') { + return; + } + if (isHast(node)) { + node.children.unshift(h('title', title)); + } else { + node.children.unshift(x('title', title)); + } + return EXIT; + }); + }; + +export const setLanguage: unified.Plugin< + [Pick<HtmlOptions, 'language' | 'contentType'>] +> = (options) => (tree) => { + if (options.language === undefined) { + return; + } + visit(tree as hast.Root | xast.Root, 'element', (node) => { + if (getTagName(node) !== 'html') { + return; + } + if (isHast(node)) { + if (node.properties?.lang) { + return EXIT; + } + node.properties = node.properties || {}; + node.properties.lang = options.language; + if (options.contentType === 'application/xhtml+xml') { + node.properties.xmlLang = options.language; + } + } else { + if (node.attributes?.lang || node.attributes?.['xml:lang']) { + return EXIT; + } + node.attributes = node.attributes || {}; + node.attributes.lang = options.language; + node.attributes['xml:lang'] = options.language; + } + return EXIT; + }); +}; + +const defaultPlugins = (options: HtmlOptions): unified.PluggableList => [ + [injectStyles, options], + [setTitle, options], + [setLanguage, options], +]; +export const defaultHtmlProcessor: HtmlProcessorFactory = (options) => + rehype() + .data('settings', { allowDangerousHtml: true }) + .use(defaultPlugins(options)); +export const defaultXhtmlProcessor: HtmlProcessorFactory = (options) => + unified() + .use(function () { + this.Parser = fromXml; + this.Compiler = (tree) => toXml(tree as xast.Root); + }) + .use(defaultPlugins(options)); + +export async function processHtml( + processorFactory: HtmlProcessorFactory, + filepath: string, + options: HtmlOptions = {}, +): Promise<VFile> { + const contents = fs.readFileSync(filepath, 'utf8'); + const processor = processorFactory(options); + return processor.process(vfile({ path: filepath, contents })); +} + +export async function processHtmlString( + processorFactory: HtmlProcessorFactory, + contents: string, + options: HtmlOptions = {}, +): Promise<VFile> { + const processor = processorFactory(options); + return processor.process(vfile({ contents })); +} diff --git a/src/processor/html.tsx b/src/processor/html.tsx index 295ec5c3..49b39d13 100644 --- a/src/processor/html.tsx +++ b/src/processor/html.tsx @@ -205,15 +205,18 @@ export async function getJsdomFromUrlOrFile({ export function getJsdomFromString({ html, + contentType, virtualConsole = createVirtualConsole((error) => { throw error; }), }: { html: string; + contentType?: 'text/html' | 'application/xhtml+xml'; virtualConsole?: VirtualConsole; }) { return new JSDOM(html, { virtualConsole, + ...(contentType && { contentType }), }); } diff --git a/tests/config.test.ts b/tests/config.test.ts index 141524ad..cad8a04a 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -508,6 +508,28 @@ it('allows non-markdown extensions when documentProcessor is provided', async () expect(xyzEntry?.title).toBe('Custom Format Title'); }); +it('does not override HTML contentType when global documentProcessor is set', async () => { + const customProcessor = () => { + throw new Error('should not be called in config parsing'); + }; + + const config = await getTaskConfig(['build'], resolveFixture('config'), { + entry: ['sample.html'], + documentProcessor: customProcessor, + }); + + const htmlEntry = config.entries.find( + (e) => + 'source' in e && + e.source?.type === 'file' && + e.source.pathname.endsWith('sample.html'), + ); + expect(htmlEntry).toBeDefined(); + expect((htmlEntry as any).contentType).toBe('text/html'); + expect((htmlEntry as any).source.documentProcessor).toBeUndefined(); + expect((htmlEntry as any).source.htmlProcessor).toBeDefined(); +}); + it('rejects text/plain files without documentProcessor', async () => { // .txt files are recognized as text/plain, which requires documentProcessor await expect( From 9c531b38a783bfd1816dc57c092e2382f4e6cfeb Mon Sep 17 00:00:00 2001 From: Koutaro Mukai <mukai.k1011k@outlook.jp> Date: Fri, 6 Feb 2026 12:44:41 +0900 Subject: [PATCH 2/4] docs: add htmlProcessor usage example to customize-processor --- examples/customize-processor/README.md | 50 +++++++++++++++++-- examples/customize-processor/package.json | 7 +-- examples/customize-processor/page.html | 18 +++++++ .../customize-processor/vivliostyle.config.js | 43 ++++++++++++++-- 4 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 examples/customize-processor/page.html diff --git a/examples/customize-processor/README.md b/examples/customize-processor/README.md index 65e109dd..70806189 100644 --- a/examples/customize-processor/README.md +++ b/examples/customize-processor/README.md @@ -15,7 +15,7 @@ import rehypeStringify from 'rehype-stringify'; import remarkRuby from 'remark-ruby'; const config = { - title: 'Markdown processor customization example', + title: 'Processor customization example', entry: ['manuscript.md'], // config is StringifyMarkdownOptions in @vivliostyle/vfm // metadata is Metadata in @vivliostyle/vfm @@ -41,12 +41,12 @@ export default config; You can also set `documentProcessor` and `documentMetadataReader` for individual entries. This allows different processing for each file: ```js -import { defineConfig, VFM, readMetadata } from '@vivliostyle/cli'; +import { defineConfig, VFM } from '@vivliostyle/cli'; import unified from 'unified'; // ... other imports const config = defineConfig({ - title: 'Markdown processor customization example', + title: 'Processor customization example', entry: [ // Uses the global documentProcessor 'manuscript.md', @@ -55,8 +55,7 @@ const config = defineConfig({ path: 'manuscript2.md', documentProcessor: VFM, documentMetadataReader: (content) => { - const match = content.match(/^#\s+(.+)$/m); - return { title: match ? match[1] : 'Untitled' }; + return { title: 'Custom title' }; }, }, ], @@ -75,3 +74,44 @@ const config = defineConfig({ export default config; ``` + +## htmlProcessor and xhtmlProcessor + +While `documentProcessor` handles Markdown-to-HTML conversion, `htmlProcessor` and `xhtmlProcessor` allow you to customize the processing of HTML and XHTML source files respectively. + +You can extend the built-in `defaultHtmlProcessor` (or `defaultXhtmlProcessor` for XHTML) with additional [rehype](https://github.com/rehypejs/rehype) plugins: + +```js +import { defineConfig, defaultHtmlProcessor } from '@vivliostyle/cli'; +import { visit } from 'unist-util-visit'; + +const openLinksInNewTab = () => (tree) => { + visit(tree, 'element', (node) => { + if ( + node.tagName === 'a' && + String(node.properties?.href).startsWith('http') + ) { + (node.properties ??= {}).target = '_blank'; + node.properties.rel = 'noopener noreferrer'; + node.children.push({ + type: 'element', + tagName: 'span', + properties: {}, + children: [{ type: 'text', value: ' ↗' }], + }); + } + }); +}; + +const config = defineConfig({ + entry: [ + { + path: 'page.html', + htmlProcessor: (options) => + defaultHtmlProcessor(options).use(openLinksInNewTab), + }, + ], +}); + +export default config; +``` diff --git a/examples/customize-processor/package.json b/examples/customize-processor/package.json index b0a4ee66..9dca8417 100644 --- a/examples/customize-processor/package.json +++ b/examples/customize-processor/package.json @@ -8,11 +8,12 @@ }, "dependencies": { "@vivliostyle/cli": "workspace:*", - "rehype-expressive-code": "^0.37.0", + "rehype-expressive-code": "0.41.6", "rehype-stringify": "^8.0.0", "remark-parse": "^9.0.0", - "remark-rehype": "^9.0.0", + "remark-rehype": "^8.1.0", "remark-ruby": "^0.4.0", - "unified": "^9.2.0" + "unified": "^9.2.0", + "unist-util-visit": "^4.1.0" } } diff --git a/examples/customize-processor/page.html b/examples/customize-processor/page.html new file mode 100644 index 00000000..f6cbce22 --- /dev/null +++ b/examples/customize-processor/page.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8" /> + <title>HTML Processor Example + + +

HTML Processor Example

+

+ This page is processed with a custom + unified processor. External links + like + Vivliostyle + will have target="_blank" and an arrow indicator added + automatically. +

+ + diff --git a/examples/customize-processor/vivliostyle.config.js b/examples/customize-processor/vivliostyle.config.js index f925bc93..312b17b0 100644 --- a/examples/customize-processor/vivliostyle.config.js +++ b/examples/customize-processor/vivliostyle.config.js @@ -1,14 +1,34 @@ // @ts-check -import { defineConfig, VFM } from '@vivliostyle/cli'; +import { defineConfig, VFM, defaultHtmlProcessor } from '@vivliostyle/cli'; import rehypeExpressiveCode from 'rehype-expressive-code'; import rehypeStringify from 'rehype-stringify'; import remarkParse from 'remark-parse'; import remark2rehype from 'remark-rehype'; import remarkRuby from 'remark-ruby'; import unified from 'unified'; +import { visit } from 'unist-util-visit'; + +/** @type {import('unified').Plugin} */ +const openLinksInNewTab = () => (tree) => { + visit(/** @type {import("hast-v2").Root} */ (tree), 'element', (node) => { + if ( + node.tagName === 'a' && + String(node.properties?.href).startsWith('http') + ) { + (node.properties ??= {}).target = '_blank'; + node.properties.rel = 'noopener noreferrer'; + node.children.push({ + type: 'element', + tagName: 'span', + properties: {}, + children: [{ type: 'text', value: ' ↗' }], + }); + } + }); +}; const config = defineConfig({ - title: 'Markdown processor customization example', + title: 'Processor customization example', entry: [ 'manuscript.md', { @@ -18,15 +38,28 @@ const config = defineConfig({ return { title: 'Custom title' }; }, }, + { + path: 'page.html', + htmlProcessor: (options) => + defaultHtmlProcessor(options).use(openLinksInNewTab), + }, ], documentProcessor: (config, metadata) => unified() .use(remarkParse) .use(remarkRuby) .use(remark2rehype) - .use(rehypeExpressiveCode, { - frames: { showCopyToClipboardButton: false }, - }) + .use( + /** + * rehype-expressive-code depends on unified@10 since its first release + * and is not strictly type-compatible with unified@9. + * It works at runtime anyway. + * @type {import("unified").Plugin<[Parameters[0]]>} + */ (rehypeExpressiveCode), + { + frames: { showCopyToClipboardButton: false }, + }, + ) .use(rehypeStringify), output: 'draft.pdf', }); From 57218e3c0a2e3c26b38fe7be49ccb61f1a63e96e Mon Sep 17 00:00:00 2001 From: Koutaro Mukai Date: Fri, 6 Feb 2026 12:47:55 +0900 Subject: [PATCH 3/4] Add changeset --- .changeset/new-crews-appear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-crews-appear.md diff --git a/.changeset/new-crews-appear.md b/.changeset/new-crews-appear.md new file mode 100644 index 00000000..e1538317 --- /dev/null +++ b/.changeset/new-crews-appear.md @@ -0,0 +1,5 @@ +--- +'@vivliostyle/cli': minor +--- + +Implement htmlProcessor/xhtmlProcessor for HTML document transformation From dbd667bcf00c4c0477792a656e0fa0590a5dc502 Mon Sep 17 00:00:00 2001 From: Koutaro Mukai Date: Sun, 8 Mar 2026 23:01:18 +0900 Subject: [PATCH 4/4] Move HtmlOptions interface to schema.ts --- docs/api-javascript.md | 12 ++++++------ docs/config.md | 16 ++++++++-------- src/config/schema.ts | 21 +++++++++++---------- src/index.ts | 2 +- src/processor/html-processor.ts | 12 +----------- 5 files changed, 27 insertions(+), 36 deletions(-) diff --git a/docs/api-javascript.md b/docs/api-javascript.md index 47f62cea..7686a281 100644 --- a/docs/api-javascript.md +++ b/docs/api-javascript.md @@ -1018,12 +1018,12 @@ Unified processor. #### Properties -| Property | Type | Description | -| ------ | ------ | ------ | -| `contentType?` | `"text/html"` \| `"application/xhtml+xml"` | Content type: 'text/html' or 'application/xhtml+xml' | -| `language?` | `string` | Document language (sets html lang if not present) | -| `style?` | `string`[] | Paths to stylesheets to inject | -| `title?` | `string` | Document title (sets if not present) | +| Property | Type | +| ------ | ------ | +| <a id="contenttype"></a> `contentType?` | `"text/html"` \| `"application/xhtml+xml"` | +| <a id="language"></a> `language?` | `string` | +| <a id="style"></a> `style?` | `string`[] | +| <a id="title"></a> `title?` | `string` | *** diff --git a/docs/config.md b/docs/config.md index 57f82c43..53bd679a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -99,10 +99,10 @@ type VivliostyleConfigSchema = - `documentMetadataReader`: (content: string) => import("@vivliostyle/vfm").Metadata Custom function to extract metadata from the source document content. - - `htmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + - `htmlProcessor`: (options: HtmlOptions) => import("unified").Processor Custom function to provide a unified Processor for transforming HTML documents. - - `xhtmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + - `xhtmlProcessor`: (options: HtmlOptions) => import("unified").Processor Custom function to provide a unified Processor for transforming XHTML documents. - `vfm`: [VfmConfig](#vfmconfig) @@ -195,10 +195,10 @@ type BuildTask = { content: string, ) => import("@vivliostyle/vfm").Metadata; htmlProcessor?: ( - options: import("../processor/html-processor.js").HtmlOptions, + options: HtmlOptions, ) => import("unified").Processor; xhtmlProcessor?: ( - options: import("../processor/html-processor.js").HtmlOptions, + options: HtmlOptions, ) => import("unified").Processor; vfm?: VfmConfig; image?: string; @@ -356,10 +356,10 @@ type CoverEntryConfig = { - `documentMetadataReader`: (content: string) => import("@vivliostyle/vfm").Metadata Custom function to extract metadata from the source document content. - - `htmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + - `htmlProcessor`: (options: HtmlOptions) => import("unified").Processor Custom function to provide a unified Processor for transforming HTML documents. - - `xhtmlProcessor`: (options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor + - `xhtmlProcessor`: (options: HtmlOptions) => import("unified").Processor Custom function to provide a unified Processor for transforming XHTML documents. #### Type definition @@ -383,10 +383,10 @@ type ArticleEntryConfig = { content: string, ) => import("@vivliostyle/vfm").Metadata; htmlProcessor?: ( - options: import("../processor/html-processor.js").HtmlOptions, + options: HtmlOptions, ) => import("unified").Processor; xhtmlProcessor?: ( - options: import("../processor/html-processor.js").HtmlOptions, + options: HtmlOptions, ) => import("unified").Processor; }; ``` diff --git a/src/config/schema.ts b/src/config/schema.ts index badfab07..125d8731 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -92,13 +92,17 @@ export const DocumentMetadataReaderSchema = v.pipe( `), ); +export interface HtmlOptions { + style?: string[]; + title?: string; + language?: string; + contentType?: 'text/html' | 'application/xhtml+xml'; +} + export const HtmlProcessorSchema = v.pipe( - v.function() as v.GenericSchema< - (options: import('../processor/html-processor.js').HtmlOptions) => Processor - >, + v.function() as v.GenericSchema<(options: HtmlOptions) => Processor>, v.metadata({ - typeString: - '(options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor', + typeString: '(options: HtmlOptions) => import("unified").Processor', }), v.description($` Custom function to provide a unified Processor for transforming HTML documents. @@ -106,12 +110,9 @@ export const HtmlProcessorSchema = v.pipe( ); export const XhtmlProcessorSchema = v.pipe( - v.function() as v.GenericSchema< - (options: import('../processor/html-processor.js').HtmlOptions) => Processor - >, + v.function() as v.GenericSchema<(options: HtmlOptions) => Processor>, v.metadata({ - typeString: - '(options: import("../processor/html-processor.js").HtmlOptions) => import("unified").Processor', + typeString: '(options: HtmlOptions) => import("unified").Processor', }), v.description($` Custom function to provide a unified Processor for transforming XHTML documents. diff --git a/src/index.ts b/src/index.ts index 82310af2..ba21d530 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,10 +18,10 @@ export type { VivliostyleConfigSchema, VivliostylePackageMetadata, } from './config/schema.js'; +export type { HtmlOptions } from './config/schema.js'; export { defaultHtmlProcessor, defaultXhtmlProcessor, - type HtmlOptions, type HtmlProcessorFactory, } from './processor/html-processor.js'; export type { TemplateVariable } from './create-template.js'; diff --git a/src/processor/html-processor.ts b/src/processor/html-processor.ts index 7f3ae5bf..71fac91d 100644 --- a/src/processor/html-processor.ts +++ b/src/processor/html-processor.ts @@ -28,17 +28,7 @@ import type * as xast from 'xast'; import { fromXml } from 'xast-util-from-xml'; import { toXml } from 'xast-util-to-xml'; import { x } from 'xastscript'; - -export interface HtmlOptions { - /** Paths to stylesheets to inject */ - style?: string[]; - /** Document title (sets <title> if not present) */ - title?: string; - /** Document language (sets html lang if not present) */ - language?: string; - /** Content type: 'text/html' or 'application/xhtml+xml' */ - contentType?: 'text/html' | 'application/xhtml+xml'; -} +import type { HtmlOptions } from '../config/schema.js'; export type HtmlProcessorFactory = (options: HtmlOptions) => unified.Processor;