@gmickel/gno 1.45.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/assets/skill/SKILL.md +7 -6
- package/assets/skill/cli-reference.md +14 -6
- package/assets/skill/mcp-reference.md +4 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +32 -33
- package/bunfig.toml +2 -0
- package/package.json +40 -26
- package/spec/cli.md +30 -11
- package/spec/db/schema.sql +146 -1
- package/spec/mcp.md +26 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +2 -0
- package/src/cli/commands/ask.ts +6 -1
- package/src/cli/commands/daemon.ts +21 -8
- package/src/cli/commands/embed.ts +77 -41
- package/src/cli/commands/mcp/install.ts +20 -0
- package/src/cli/commands/mcp/paths.ts +25 -0
- package/src/cli/commands/mcp/status.ts +6 -0
- package/src/cli/detach.ts +3 -2
- package/src/cli/program.ts +6 -0
- package/src/config/types.ts +3 -3
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
- package/src/converters/adapters/officeparser/adapter.ts +1 -2
- package/src/converters/versions.ts +6 -8
- package/src/core/context-evidence.ts +8 -4
- package/src/core/job-manager.ts +95 -13
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/core/shutdown-budget.ts +45 -0
- package/src/embed/backlog.ts +107 -4
- package/src/embed/batch.ts +42 -2
- package/src/embed/fingerprint.ts +16 -0
- package/src/embed/retry.ts +113 -5
- package/src/embed/variant-backlog.ts +105 -0
- package/src/embed/variant-plan.ts +62 -0
- package/src/embed/variant-retry.ts +113 -0
- package/src/ingestion/graph-reconciliation.ts +327 -0
- package/src/ingestion/sync.ts +9 -272
- package/src/llm/http-inference.ts +6 -0
- package/src/llm/httpEmbedding.ts +37 -6
- package/src/llm/httpGeneration.ts +18 -3
- package/src/llm/httpRerank.ts +23 -5
- package/src/llm/inference-cancellation.ts +168 -0
- package/src/llm/inference-scope.ts +202 -0
- package/src/llm/lazy-ports.ts +115 -0
- package/src/llm/native-worker/client.ts +541 -0
- package/src/llm/native-worker/dispatcher.ts +228 -0
- package/src/llm/native-worker/embedding-identity.ts +33 -0
- package/src/llm/native-worker/entry.ts +173 -0
- package/src/llm/native-worker/errors.ts +32 -0
- package/src/llm/native-worker/evaluation.ts +16 -0
- package/src/llm/native-worker/owned-exit.ts +108 -0
- package/src/llm/native-worker/owner.ts +141 -0
- package/src/llm/native-worker/ports.ts +317 -0
- package/src/llm/native-worker/protocol.ts +442 -0
- package/src/llm/native-worker/runtime-config.ts +92 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
- package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
- package/src/llm/nodeLlamaCpp/generation.ts +34 -5
- package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
- package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
- package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
- package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
- package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
- package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
- package/src/llm/types.ts +35 -5
- package/src/mcp/context.ts +27 -0
- package/src/mcp/http-transport.ts +12 -10
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-profile.ts +30 -8
- package/src/mcp/tools/context.ts +8 -11
- package/src/mcp/tools/embed.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +1 -1
- package/src/mcp/tools/index.ts +10 -8
- package/src/mcp/tools/query.ts +14 -30
- package/src/mcp/tools/vsearch.ts +1 -1
- package/src/pipeline/answer.ts +23 -3
- package/src/pipeline/claim-verifier.ts +6 -0
- package/src/pipeline/expansion.ts +43 -40
- package/src/pipeline/explain.ts +6 -2
- package/src/pipeline/filters.ts +63 -0
- package/src/pipeline/fusion.ts +29 -9
- package/src/pipeline/graph-retrieval.ts +29 -9
- package/src/pipeline/hybrid.ts +198 -55
- package/src/pipeline/hydration.ts +161 -0
- package/src/pipeline/owner-fusion.ts +87 -0
- package/src/pipeline/rerank.ts +35 -11
- package/src/pipeline/search.ts +13 -2
- package/src/pipeline/types.ts +5 -3
- package/src/pipeline/vsearch.ts +87 -7
- package/src/sdk/client.ts +47 -3
- package/src/sdk/embed.ts +63 -39
- package/src/serve/background-runtime.ts +1 -1
- package/src/serve/context.ts +41 -56
- package/src/serve/embed-scheduler.ts +58 -35
- package/src/serve/public/components/IndexingProgress.tsx +46 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/shiki-language-ids.ts +14 -0
- package/src/serve/resident-admission.ts +36 -36
- package/src/serve/resident-background-work.ts +20 -2
- package/src/serve/resident-request.ts +11 -5
- package/src/serve/resident-runtime.ts +97 -61
- package/src/serve/resident-shutdown.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +47 -26
- package/src/store/migrations/028-vector-variants.ts +54 -0
- package/src/store/migrations/029-graph-reference-state.ts +77 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +251 -183
- package/src/store/sqlite/eligibility.ts +174 -0
- package/src/store/sqlite/graph-edge-application.ts +66 -0
- package/src/store/sqlite/graph-reference-state.ts +194 -0
- package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
- package/src/store/types.ts +80 -12
- package/src/store/vector/eligibility.ts +36 -0
- package/src/store/vector/freshness.ts +33 -6
- package/src/store/vector/lazy.ts +81 -0
- package/src/store/vector/sqlite-vec.ts +106 -54
- package/src/store/vector/stats.ts +14 -3
- package/src/store/vector/types.ts +35 -2
- package/src/store/vector/variant-search.ts +192 -0
- package/src/store/vector/variants.ts +451 -0
- package/vendor/converters/markitdown-ts/LICENSE +21 -0
- package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
- package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
- package/vendor/converters/markitdown-ts/package.json +77 -0
- package/vendor/converters/officeparser/LICENSE +21 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
- package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
- package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
- package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
- package/vendor/converters/officeparser/dist/cli.js +381 -0
- package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
- package/vendor/converters/officeparser/dist/defaults.js +218 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
- package/vendor/converters/officeparser/dist/index.d.ts +60 -0
- package/vendor/converters/officeparser/dist/index.js +72 -0
- package/vendor/converters/officeparser/dist/index.mjs +18 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
- package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
- package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
- package/vendor/converters/officeparser/dist/types.js +107 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
- package/vendor/converters/officeparser/package.json +147 -0
- package/vendor/converters/upstream-manifest.json +124 -0
- package/vendor/dependency-fixes/README.md +77 -0
- package/vendor/dependency-fixes/vendor-converters.py +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-627emwpj.js +0 -75
- /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared output-sanitization helpers.
|
|
4
|
+
*
|
|
5
|
+
* Every string in the parsed AST originates from an untrusted document, so any
|
|
6
|
+
* value interpolated into generated output (HTML, XHTML, CSS, URLs, inline
|
|
7
|
+
* scripts, CSV, RTF, Markdown) must be escaped for its destination context.
|
|
8
|
+
* These are the single source of truth — each generator delegates to them so
|
|
9
|
+
* escaping stays consistent and a gap fixed here is fixed everywhere.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.isSafeHtmlAttributeName = isSafeHtmlAttributeName;
|
|
13
|
+
exports.isSafeStyleMapTag = isSafeStyleMapTag;
|
|
14
|
+
exports.escapeHtml = escapeHtml;
|
|
15
|
+
exports.escapeXml = escapeXml;
|
|
16
|
+
exports.sanitizeCssValue = sanitizeCssValue;
|
|
17
|
+
exports.sanitizeUrl = sanitizeUrl;
|
|
18
|
+
exports.iframeAllowed = iframeAllowed;
|
|
19
|
+
exports.sanitizeImageUrl = sanitizeImageUrl;
|
|
20
|
+
exports.serializeForInlineScript = serializeForInlineScript;
|
|
21
|
+
exports.csvSafeCell = csvSafeCell;
|
|
22
|
+
exports.sanitizeRtfUrl = sanitizeRtfUrl;
|
|
23
|
+
exports.escapeRtf = escapeRtf;
|
|
24
|
+
exports.markdownEscapeText = markdownEscapeText;
|
|
25
|
+
exports.sanitizeMarkdownUrl = sanitizeMarkdownUrl;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a string is a plain HTML attribute *name*, safe to interpolate before `="..."`.
|
|
28
|
+
*
|
|
29
|
+
* Escaping the value is not enough on its own: a key containing a quote or `=` closes the
|
|
30
|
+
* attribute and opens another, so `x" onmouseover="alert(1)" z` yields a real event handler no
|
|
31
|
+
* matter how carefully the value is escaped. This is the shape an attribute-injection payload
|
|
32
|
+
* takes, and rejecting it outright is simpler and safer than trying to escape a name.
|
|
33
|
+
*
|
|
34
|
+
* The predicate is shared rather than restated because it is now applied at four independent
|
|
35
|
+
* points (the parser's attribute collection, the generator's attribute bag, and two
|
|
36
|
+
* styleMap-driven paths). Each of those still keeps its own skip-list inline: the lists are the
|
|
37
|
+
* same policy expressed for different layers, and collapsing them would erase the defence in
|
|
38
|
+
* depth the surrounding comments describe.
|
|
39
|
+
*/
|
|
40
|
+
function isSafeHtmlAttributeName(name) {
|
|
41
|
+
return typeof name === 'string' && /^[a-zA-Z][a-zA-Z0-9-]*$/.test(name);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Element names a `styleMap` may map a node onto.
|
|
45
|
+
*
|
|
46
|
+
* An allowlist rather than a pattern: a tag name is interpolated into both `<TAG …>` and
|
|
47
|
+
* `</TAG>`, so it is not enough for it to *look* like a name - `script`, `style`, `iframe` and
|
|
48
|
+
* friends are perfectly well-formed names that would introduce an active context the rest of the
|
|
49
|
+
* generator's escaping assumes does not exist. This is the semantic set a style mapping is for:
|
|
50
|
+
* block containers, headings, and the inline emphasis elements.
|
|
51
|
+
*/
|
|
52
|
+
const SAFE_STYLE_MAP_TAGS = new Set([
|
|
53
|
+
'p', 'div', 'span', 'section', 'article', 'aside', 'header', 'footer', 'main', 'figure', 'figcaption',
|
|
54
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
55
|
+
'blockquote', 'pre', 'code', 'q', 'cite', 'address',
|
|
56
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
57
|
+
'b', 'strong', 'i', 'em', 'u', 'ins', 'del', 's', 'strike', 'mark', 'small', 'sub', 'sup', 'kbd', 'samp', 'var', 'abbr',
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Whether a `styleMap` `output.tag` may be emitted as an element name.
|
|
61
|
+
*
|
|
62
|
+
* Callers must fall back to their default tag when this returns false, never emit the value.
|
|
63
|
+
*/
|
|
64
|
+
function isSafeStyleMapTag(tag) {
|
|
65
|
+
return typeof tag === 'string' && SAFE_STYLE_MAP_TAGS.has(tag.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Escapes text for an HTML text node or a double-quoted attribute value.
|
|
69
|
+
* Includes the single quote so the result is also safe inside single-quoted
|
|
70
|
+
* attributes.
|
|
71
|
+
*/
|
|
72
|
+
function escapeHtml(text) {
|
|
73
|
+
if (typeof text !== 'string')
|
|
74
|
+
return text;
|
|
75
|
+
return text
|
|
76
|
+
.replace(/&/g, '&')
|
|
77
|
+
.replace(/</g, '<')
|
|
78
|
+
.replace(/>/g, '>')
|
|
79
|
+
.replace(/"/g, '"')
|
|
80
|
+
.replace(/'/g, ''');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Escapes text for an XML text node or attribute (XHTML/OPF/NCX). Same as
|
|
84
|
+
* escapeHtml but emits the XML-canonical `'` for the single quote.
|
|
85
|
+
*/
|
|
86
|
+
function escapeXml(text) {
|
|
87
|
+
if (typeof text !== 'string')
|
|
88
|
+
return '';
|
|
89
|
+
return text
|
|
90
|
+
.replace(/&/g, '&')
|
|
91
|
+
.replace(/</g, '<')
|
|
92
|
+
.replace(/>/g, '>')
|
|
93
|
+
.replace(/"/g, '"')
|
|
94
|
+
.replace(/'/g, ''');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sanitizes a single CSS value (e.g. a color/size/font/alignment pulled from a
|
|
98
|
+
* document) for placement inside a `style="prop: VALUE"` attribute.
|
|
99
|
+
*
|
|
100
|
+
* - Drops the whole value if it contains a resource-fetching or executing
|
|
101
|
+
* construct (`url()`, `expression()`, `@import`, `image-set()`, `javascript:`)
|
|
102
|
+
* or angle brackets that could break out of the attribute/tag.
|
|
103
|
+
* - Strips characters that break out of `prop: value` (`;`, quotes), out of a
|
|
104
|
+
* `<style>` rule (`{}`), CSS escapes (`\`), and control characters.
|
|
105
|
+
*
|
|
106
|
+
* `rgb()/hsl()` and hex/named colors, lengths, and (unquoted) font names all
|
|
107
|
+
* survive; the trade-off is that legitimately quoted font names lose their
|
|
108
|
+
* quotes, which browsers tolerate.
|
|
109
|
+
*/
|
|
110
|
+
function sanitizeCssValue(value) {
|
|
111
|
+
if (typeof value !== 'string')
|
|
112
|
+
return '';
|
|
113
|
+
// Strip every form of intra-token noise FIRST, then test for dangerous constructs.
|
|
114
|
+
// Order matters: a payload like "u\nrl(", "url/*x*/(" or "u\rl(" would survive the test if
|
|
115
|
+
// tested before removal, then reassemble into "url(" once the noise is stripped.
|
|
116
|
+
//
|
|
117
|
+
// The backslash strip belongs here, not after the test. CSS treats `\` as an escape a
|
|
118
|
+
// browser resolves away, so `u\rl(http://evil)` IS `url(http://evil)` to a renderer -
|
|
119
|
+
// stripping it downstream of the test meant the sanitizer handed back a live `url()` it
|
|
120
|
+
// had just declared safe. Every construct in the denylist is reachable this way
|
|
121
|
+
// (`expr\ession(`, `image\-set(`), so the fix is the ordering, not another pattern.
|
|
122
|
+
const cleaned = value
|
|
123
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // control chars (incl. newlines/tabs)
|
|
124
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // CSS comments used to obfuscate
|
|
125
|
+
.replace(/\\/g, ''); // CSS escapes; see above
|
|
126
|
+
if (/(?:url|expression|image-set|element|-moz-binding)\s*\(|@import|javascript:|[<>]/i.test(cleaned)) {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
// Backslash is already gone above; the rest still have work to do here.
|
|
130
|
+
return cleaned.replace(/[;{}"'`]/g, '').trim();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Escapes a document-supplied URL for use in an href/src attribute. Beyond the
|
|
134
|
+
* usual attribute escaping, this rejects script-executing schemes (javascript:,
|
|
135
|
+
* vbscript:, data:, etc.) so a hyperlink extracted from an untrusted document
|
|
136
|
+
* can't run code when clicked — only http(s)/mailto/tel and relative/fragment
|
|
137
|
+
* URLs are passed through.
|
|
138
|
+
*/
|
|
139
|
+
function sanitizeUrl(url) {
|
|
140
|
+
if (typeof url !== 'string')
|
|
141
|
+
return '';
|
|
142
|
+
const trimmed = url.trim();
|
|
143
|
+
// Browsers ignore control characters when parsing a URL scheme, so strip them
|
|
144
|
+
// first to catch obfuscated payloads like "java\tscript:alert(1)".
|
|
145
|
+
const stripped = trimmed.replace(/[\x00-\x1F\x7F]+/g, '');
|
|
146
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(stripped);
|
|
147
|
+
if (schemeMatch && !/^(https?|mailto|tel)$/i.test(schemeMatch[1])) {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
// Emit the same normalized string that was validated.
|
|
151
|
+
return escapeHtml(stripped);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decide whether a non-provider `<iframe>` should be preserved, per
|
|
155
|
+
* `HtmlParserConfig.preserveIframes`. `true` allows any src; an array is a hostname allowlist,
|
|
156
|
+
* where an entry matches the src's host exactly or as a `.`-suffix (so `"vimeo.com"` also matches
|
|
157
|
+
* `player.vimeo.com`). A relative or unparseable src is allowed only under `true`. This is a
|
|
158
|
+
* preservation gate, not a sanitizer - the src is still scheme-checked with `sanitizeUrl` on
|
|
159
|
+
* generation.
|
|
160
|
+
*/
|
|
161
|
+
function iframeAllowed(src, preserve) {
|
|
162
|
+
if (preserve === true)
|
|
163
|
+
return true;
|
|
164
|
+
if (!Array.isArray(preserve) || preserve.length === 0)
|
|
165
|
+
return false;
|
|
166
|
+
let host;
|
|
167
|
+
try {
|
|
168
|
+
host = new URL(src).hostname.toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return preserve.some(entry => {
|
|
174
|
+
const e = String(entry).toLowerCase().trim();
|
|
175
|
+
return e.length > 0 && (host === e || host.endsWith('.' + e));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Like sanitizeUrl but for an <img>/<source> src: additionally permits
|
|
180
|
+
* `data:image/*` URIs (embedded document images) while still rejecting
|
|
181
|
+
* script-executing schemes and non-image data URIs (e.g. data:text/html).
|
|
182
|
+
*/
|
|
183
|
+
function sanitizeImageUrl(url) {
|
|
184
|
+
if (typeof url !== 'string')
|
|
185
|
+
return '';
|
|
186
|
+
const trimmed = url.trim();
|
|
187
|
+
const stripped = trimmed.replace(/[\x00-\x1F\x7F]+/g, '');
|
|
188
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(stripped);
|
|
189
|
+
if (schemeMatch) {
|
|
190
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
191
|
+
if (scheme === 'data') {
|
|
192
|
+
if (!/^data:image\//i.test(stripped))
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
else if (scheme !== 'http' && scheme !== 'https') {
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return escapeHtml(stripped);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Serializes data for embedding inside an inline <script> block. JSON.stringify
|
|
203
|
+
* alone doesn't escape "<", so a value containing "</script>" (e.g. a chart
|
|
204
|
+
* label from attacker-controlled document XML) would close the script early and
|
|
205
|
+
* inject markup. Also escapes the U+2028/U+2029 line separators, which are
|
|
206
|
+
* invalid in JS string literals.
|
|
207
|
+
*/
|
|
208
|
+
function serializeForInlineScript(data) {
|
|
209
|
+
// U+2028/U+2029 (line/paragraph separators) are valid in JSON but break
|
|
210
|
+
// JS string literals; reference them by code point to keep the source ASCII.
|
|
211
|
+
const lineSep = String.fromCharCode(0x2028);
|
|
212
|
+
const paraSep = String.fromCharCode(0x2029);
|
|
213
|
+
return JSON.stringify(data)
|
|
214
|
+
.replace(/</g, '\\u003C')
|
|
215
|
+
.replace(/>/g, '\\u003E')
|
|
216
|
+
.split(lineSep).join('\\u2028')
|
|
217
|
+
.split(paraSep).join('\\u2029');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Formats a value for a CSV field: guards against spreadsheet formula/DDE
|
|
221
|
+
* injection (CWE-1236) and applies RFC 4180 quoting.
|
|
222
|
+
*
|
|
223
|
+
* A cell beginning with `= + - @` (or a tab/CR that some apps treat as a
|
|
224
|
+
* formula start) is prefixed with a single quote so Excel/Sheets render it as
|
|
225
|
+
* literal text rather than executing it. Genuine numbers (including negatives)
|
|
226
|
+
* are exempt so numeric columns are preserved.
|
|
227
|
+
*/
|
|
228
|
+
function csvSafeCell(value, delimiter) {
|
|
229
|
+
let v = typeof value === 'string' ? value : String(value ?? '');
|
|
230
|
+
// A plain signed number (e.g. "-8", "+7", "-5.3") can't be a formula, so exempt it —
|
|
231
|
+
// otherwise numeric columns get quoted as text. Anything else starting with a formula
|
|
232
|
+
// trigger (including "+1+1", "-1+cmd", "=", "@") is prefixed with a quote.
|
|
233
|
+
const isNumber = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(v.trim());
|
|
234
|
+
// Test the trimmed value: the numeric exemption above already trims, so testing the raw
|
|
235
|
+
// string here meant a leading space slipped a trigger past the guard (" =1+1" was emitted
|
|
236
|
+
// unprefixed). Most spreadsheet apps treat a leading-space cell as text and would not
|
|
237
|
+
// evaluate it, so this is defence in depth rather than a demonstrated bypass - but the
|
|
238
|
+
// asymmetry between the two tests was an accident, not a decision.
|
|
239
|
+
if (!isNumber && /^[=+\-@\t\r]/.test(v.trim())) {
|
|
240
|
+
v = `'${v}`;
|
|
241
|
+
}
|
|
242
|
+
if (v.includes(delimiter) || v.includes('"') || v.includes('\n') || v.includes('\r')) {
|
|
243
|
+
return `"${v.replace(/"/g, '""')}"`;
|
|
244
|
+
}
|
|
245
|
+
return v;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Validates and escapes a document-supplied URL for an RTF `HYPERLINK` field argument.
|
|
249
|
+
*
|
|
250
|
+
* Mirrors `sanitizeUrl`'s contract (validate the scheme, then encode for the destination, else
|
|
251
|
+
* return `''`) but cannot reuse it: `sanitizeUrl` returns `escapeHtml(...)`, which would emit
|
|
252
|
+
* `&` into an RTF field. The scheme allowlist is deliberately identical to `sanitizeUrl`'s
|
|
253
|
+
* and `sanitizeMarkdownUrl`'s, so all three text generators agree on what a hyperlink may point at.
|
|
254
|
+
*
|
|
255
|
+
* **Additionally rejects UNC paths (`\\host\share`), which the HTML allowlist does not.** In a
|
|
256
|
+
* browser `\\evil.com\share` is an inert relative path; in Word it is a live UNC reference that
|
|
257
|
+
* triggers an SMB fetch and an NTLM handshake on click, which is a credential-leak vector rather
|
|
258
|
+
* than a rendering quirk. That asymmetry is why this is a separate function and not a flag on
|
|
259
|
+
* `sanitizeUrl` - the HTML helper must NOT gain this behaviour, since there the path is harmless
|
|
260
|
+
* and rejecting it would break legitimate relative links.
|
|
261
|
+
*
|
|
262
|
+
* Returns `''` for a rejected URL; callers emit the link text without the field wrapper, matching
|
|
263
|
+
* how HTML degrades to `href=""` and Markdown to `[text]()`.
|
|
264
|
+
*/
|
|
265
|
+
function sanitizeRtfUrl(url) {
|
|
266
|
+
if (typeof url !== 'string')
|
|
267
|
+
return '';
|
|
268
|
+
const trimmed = url.trim();
|
|
269
|
+
// Control characters are stripped before scheme matching for the same reason as sanitizeUrl:
|
|
270
|
+
// they are ignored when the target application parses the scheme.
|
|
271
|
+
const stripped = trimmed.replace(/[\x00-\x1F\x7F]+/g, '');
|
|
272
|
+
if (/^[\\/]{2}[^\\/]/.test(stripped))
|
|
273
|
+
return ''; // UNC (\\host\share, //host\share)
|
|
274
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(stripped);
|
|
275
|
+
if (schemeMatch && !/^(https?|mailto|tel)$/i.test(schemeMatch[1])) {
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
return escapeRtf(stripped);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Escapes text for RTF: neutralizes the control/group metacharacters `\ { }`
|
|
282
|
+
* (which would otherwise inject RTF control words or groups), encodes the double
|
|
283
|
+
* quote (so a hyperlink field argument can't be terminated early), and hex/unicode
|
|
284
|
+
* encodes non-ASCII characters.
|
|
285
|
+
*/
|
|
286
|
+
function escapeRtf(text) {
|
|
287
|
+
if (typeof text !== 'string')
|
|
288
|
+
return '';
|
|
289
|
+
return text
|
|
290
|
+
.replace(/\\/g, '\\\\')
|
|
291
|
+
.replace(/{/g, '\\{')
|
|
292
|
+
.replace(/}/g, '\\}')
|
|
293
|
+
.replace(/"/g, "\\'22")
|
|
294
|
+
.replace(/[^\x00-\x7F]/g, (match) => {
|
|
295
|
+
let code = match.charCodeAt(0);
|
|
296
|
+
if (code < 256) {
|
|
297
|
+
return `\\'${code.toString(16).padStart(2, '0')}`;
|
|
298
|
+
}
|
|
299
|
+
if (code > 32767) {
|
|
300
|
+
code -= 65536;
|
|
301
|
+
}
|
|
302
|
+
return `{\\uc0\\u${code}}`;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Escapes document text for a Markdown text position. Markdown passes raw HTML
|
|
307
|
+
* through to the renderer, so a `<` that begins an HTML tag or comment must be
|
|
308
|
+
* neutralized to prevent `<script>`/`<img onerror>` injection when the Markdown
|
|
309
|
+
* is later rendered to HTML.
|
|
310
|
+
*
|
|
311
|
+
* Deliberately narrow — only a `<` immediately followed by a letter, `/`, `!` or
|
|
312
|
+
* `?` (i.e. one that actually opens a tag/comment/PI, matching how browsers
|
|
313
|
+
* detect tags) is encoded. A bare `<` (e.g. `a < b`), `>`, `&`, `[]` and other
|
|
314
|
+
* Markdown metacharacters are left untouched: they can't start a tag, and
|
|
315
|
+
* MarkdownParser round-trips this output without decoding entities, so encoding
|
|
316
|
+
* them would corrupt re-parsed content. URL schemes are handled by
|
|
317
|
+
* sanitizeMarkdownUrl.
|
|
318
|
+
*/
|
|
319
|
+
function markdownEscapeText(text) {
|
|
320
|
+
if (typeof text !== 'string')
|
|
321
|
+
return '';
|
|
322
|
+
return text.replace(/<(?=[a-zA-Z/!?])/g, '<');
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Sanitizes a document-supplied URL for a Markdown `[text](url)` / ``
|
|
326
|
+
* target. Rejects script-executing schemes (returning '' → a dead link) and
|
|
327
|
+
* percent-encodes the characters that would break out of the `(...)` or inject
|
|
328
|
+
* markup. `&` is preserved so query strings survive; set `allowDataImage` for
|
|
329
|
+
* image targets so embedded `data:image/*` URIs are permitted.
|
|
330
|
+
*/
|
|
331
|
+
function sanitizeMarkdownUrl(url, opts) {
|
|
332
|
+
if (typeof url !== 'string')
|
|
333
|
+
return '';
|
|
334
|
+
const stripped = url.trim().replace(/[\x00-\x1F\x7F]+/g, '');
|
|
335
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(stripped);
|
|
336
|
+
if (schemeMatch) {
|
|
337
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
338
|
+
const ok = /^(?:https?|mailto|tel)$/.test(scheme)
|
|
339
|
+
|| (opts?.allowDataImage === true && /^data:image\//i.test(stripped));
|
|
340
|
+
if (!ok)
|
|
341
|
+
return '';
|
|
342
|
+
}
|
|
343
|
+
return stripped.replace(/[\s()<>"`\\]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
344
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a range string (e.g., "1", "1-3", "1,2", "1,3-5, 7") into an array of numbers.
|
|
3
|
+
*
|
|
4
|
+
* @param rangeStr - The range string to parse
|
|
5
|
+
* @returns An array of unique, sorted numbers (1-based indices)
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseRangeString(rangeStr: string): number[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRangeString = parseRangeString;
|
|
4
|
+
/**
|
|
5
|
+
* Parses a range string (e.g., "1", "1-3", "1,2", "1,3-5, 7") into an array of numbers.
|
|
6
|
+
*
|
|
7
|
+
* @param rangeStr - The range string to parse
|
|
8
|
+
* @returns An array of unique, sorted numbers (1-based indices)
|
|
9
|
+
*/
|
|
10
|
+
function parseRangeString(rangeStr) {
|
|
11
|
+
const result = new Set();
|
|
12
|
+
const segments = rangeStr.split(',');
|
|
13
|
+
for (const segment of segments) {
|
|
14
|
+
const trimmed = segment.trim();
|
|
15
|
+
if (trimmed.includes('-')) {
|
|
16
|
+
const [startStr, endStr] = trimmed.split('-');
|
|
17
|
+
const start = parseInt(startStr, 10);
|
|
18
|
+
const end = parseInt(endStr, 10);
|
|
19
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
20
|
+
const actualStart = Math.min(start, end);
|
|
21
|
+
const actualEnd = Math.max(start, end);
|
|
22
|
+
for (let i = actualStart; i <= actualEnd; i++) {
|
|
23
|
+
result.add(i);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const val = parseInt(trimmed, 10);
|
|
29
|
+
if (!isNaN(val)) {
|
|
30
|
+
result.add(val);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Array.from(result).sort((a, b) => a - b);
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { OfficeContentNode, StructuredStyleMapping } from '../types.js';
|
|
2
|
+
export interface StyleMapping {
|
|
3
|
+
selector: {
|
|
4
|
+
nodeType?: string;
|
|
5
|
+
attributes: Record<string, {
|
|
6
|
+
value: string | number | boolean;
|
|
7
|
+
operator: '=' | '~=';
|
|
8
|
+
compiled?: RegExp;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
output: {
|
|
12
|
+
tag: string;
|
|
13
|
+
classes: string[];
|
|
14
|
+
attributes: Record<string, string>;
|
|
15
|
+
fresh: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parser and matcher for the style mapping DSL.
|
|
20
|
+
* Supports a structured JSON format and a legacy string DSL.
|
|
21
|
+
*/
|
|
22
|
+
export declare class StyleMapper {
|
|
23
|
+
private mappings;
|
|
24
|
+
constructor(mappings?: string[] | StructuredStyleMapping[] | Record<string, any>, ignoreDefaults?: boolean);
|
|
25
|
+
/**
|
|
26
|
+
* Finds the best matching mapping for a node.
|
|
27
|
+
*/
|
|
28
|
+
getMapping(node: OfficeContentNode): StyleMapping['output'] | undefined;
|
|
29
|
+
private matches;
|
|
30
|
+
private getNodeAttribute;
|
|
31
|
+
private convertStructuredMapping;
|
|
32
|
+
/**
|
|
33
|
+
* Parses a mapping string like "p[style-name='Heading 1'] => h1.title:fresh"
|
|
34
|
+
*/
|
|
35
|
+
private parseMappingString;
|
|
36
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StyleMapper = void 0;
|
|
4
|
+
const types_js_1 = require("../types.js");
|
|
5
|
+
const errorUtils_js_1 = require("./errorUtils.js");
|
|
6
|
+
const DEFAULT_MAPPINGS = [
|
|
7
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Heading 1' } }, output: { tag: 'h1' } },
|
|
8
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Heading 2' } }, output: { tag: 'h2' } },
|
|
9
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Heading 3' } }, output: { tag: 'h3' } },
|
|
10
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Heading 4' } }, output: { tag: 'h4' } },
|
|
11
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Heading 5' } }, output: { tag: 'h5' } },
|
|
12
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Heading 6' } }, output: { tag: 'h6' } },
|
|
13
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Title' } }, output: { tag: 'h1', classes: ['title'] } },
|
|
14
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Subtitle' } }, output: { tag: 'p', classes: ['subtitle'] } },
|
|
15
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Quote' } }, output: { tag: 'blockquote' } },
|
|
16
|
+
{ selector: { nodeType: 'paragraph', attributes: { 'style-name': 'Intense Quote' } }, output: { tag: 'blockquote', classes: ['intense'] } },
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Parser and matcher for the style mapping DSL.
|
|
20
|
+
* Supports a structured JSON format and a legacy string DSL.
|
|
21
|
+
*/
|
|
22
|
+
class StyleMapper {
|
|
23
|
+
mappings = [];
|
|
24
|
+
constructor(mappings, ignoreDefaults = false) {
|
|
25
|
+
// 1. Add user mappings (they take precedence)
|
|
26
|
+
if (mappings) {
|
|
27
|
+
if (Array.isArray(mappings)) {
|
|
28
|
+
for (const m of mappings) {
|
|
29
|
+
if (typeof m === 'string') {
|
|
30
|
+
this.mappings.push(this.parseMappingString(m));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.mappings.push(this.convertStructuredMapping(m));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Support legacy object format: { 'Heading 1': { tag: 'h1', class: 'title' } }
|
|
39
|
+
for (const [styleName, target] of Object.entries(mappings)) {
|
|
40
|
+
this.mappings.push({
|
|
41
|
+
selector: {
|
|
42
|
+
attributes: { style: { value: styleName, operator: '=' } }
|
|
43
|
+
},
|
|
44
|
+
output: {
|
|
45
|
+
tag: target.tag || 'div',
|
|
46
|
+
classes: target.class ? target.class.split(' ') : [],
|
|
47
|
+
attributes: {},
|
|
48
|
+
fresh: false
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 2. Add default mappings if not ignored
|
|
55
|
+
if (!ignoreDefaults) {
|
|
56
|
+
this.mappings.push(...DEFAULT_MAPPINGS.map(m => this.convertStructuredMapping(m)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Finds the best matching mapping for a node.
|
|
61
|
+
*/
|
|
62
|
+
getMapping(node) {
|
|
63
|
+
for (const mapping of this.mappings) {
|
|
64
|
+
if (this.matches(node, mapping.selector)) {
|
|
65
|
+
return mapping.output;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
matches(node, selector) {
|
|
71
|
+
// Match node type if specified
|
|
72
|
+
if (selector.nodeType && node.type !== selector.nodeType) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// Match attributes (style, level, etc.)
|
|
76
|
+
for (const [attr, { value, operator, compiled }] of Object.entries(selector.attributes)) {
|
|
77
|
+
const actualValue = this.getNodeAttribute(node, attr);
|
|
78
|
+
if (actualValue === undefined)
|
|
79
|
+
return false;
|
|
80
|
+
if (operator === '=') {
|
|
81
|
+
if (String(actualValue) !== String(value))
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
else if (operator === '~=') {
|
|
85
|
+
const regex = compiled || new RegExp(String(value));
|
|
86
|
+
if (!regex.test(String(actualValue)))
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
getNodeAttribute(node, attr) {
|
|
93
|
+
// Special case for style (alias style-name for mammoth.js compatibility)
|
|
94
|
+
if (attr === 'style' || attr === 'style-name') {
|
|
95
|
+
return node.metadata?.style || node.formatting?.font;
|
|
96
|
+
}
|
|
97
|
+
// Metadata attributes
|
|
98
|
+
if (node.metadata && attr in node.metadata) {
|
|
99
|
+
return node.metadata[attr];
|
|
100
|
+
}
|
|
101
|
+
// Formatting attributes
|
|
102
|
+
if (node.formatting && attr in node.formatting) {
|
|
103
|
+
return node.formatting[attr];
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
convertStructuredMapping(m) {
|
|
108
|
+
const attributes = {};
|
|
109
|
+
if (m.selector.attributes) {
|
|
110
|
+
for (const [key, val] of Object.entries(m.selector.attributes)) {
|
|
111
|
+
if (typeof val === 'object' && val !== null && 'value' in val) {
|
|
112
|
+
const operator = val.operator || '=';
|
|
113
|
+
attributes[key] = {
|
|
114
|
+
value: val.value,
|
|
115
|
+
operator,
|
|
116
|
+
compiled: operator === '~=' ? new RegExp(String(val.value)) : undefined
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
attributes[key] = {
|
|
121
|
+
value: val,
|
|
122
|
+
operator: '='
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
selector: {
|
|
129
|
+
nodeType: m.selector.nodeType,
|
|
130
|
+
attributes
|
|
131
|
+
},
|
|
132
|
+
output: {
|
|
133
|
+
tag: m.output.tag,
|
|
134
|
+
classes: m.output.classes || [],
|
|
135
|
+
attributes: m.output.attributes || {},
|
|
136
|
+
fresh: m.output.fresh || false
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Parses a mapping string like "p[style-name='Heading 1'] => h1.title:fresh"
|
|
142
|
+
*/
|
|
143
|
+
parseMappingString(mapping) {
|
|
144
|
+
const lastIndex = mapping.lastIndexOf('=>');
|
|
145
|
+
if (lastIndex === -1) {
|
|
146
|
+
throw (0, errorUtils_js_1.getOfficeError)(types_js_1.OfficeErrorType.INVALID_STYLE_MAPPING, undefined, mapping);
|
|
147
|
+
}
|
|
148
|
+
const selectorStr = mapping.substring(0, lastIndex).trim();
|
|
149
|
+
const outputStr = mapping.substring(lastIndex + 2).trim();
|
|
150
|
+
// Parse Selector
|
|
151
|
+
const selectorMatch = selectorStr.match(/^([a-z]+)?(?:\[(.+?)\])?$/);
|
|
152
|
+
if (!selectorMatch) {
|
|
153
|
+
throw (0, errorUtils_js_1.getOfficeError)(types_js_1.OfficeErrorType.INVALID_SELECTOR, undefined, selectorStr);
|
|
154
|
+
}
|
|
155
|
+
const typeMap = {
|
|
156
|
+
'p': 'paragraph',
|
|
157
|
+
'h': 'heading',
|
|
158
|
+
't': 'table',
|
|
159
|
+
'tr': 'row',
|
|
160
|
+
'td': 'cell',
|
|
161
|
+
'li': 'list',
|
|
162
|
+
'img': 'image'
|
|
163
|
+
};
|
|
164
|
+
const nodeType = selectorMatch[1] ? (typeMap[selectorMatch[1]] || selectorMatch[1]) : undefined;
|
|
165
|
+
const attrStr = selectorMatch[2];
|
|
166
|
+
const attributes = {};
|
|
167
|
+
if (attrStr) {
|
|
168
|
+
// Improved attribute parsing to handle commas inside quotes
|
|
169
|
+
const attrParts = [];
|
|
170
|
+
let currentPart = '';
|
|
171
|
+
let inQuotes = false;
|
|
172
|
+
for (let i = 0; i < attrStr.length; i++) {
|
|
173
|
+
const char = attrStr[i];
|
|
174
|
+
if (char === "'" || char === '"')
|
|
175
|
+
inQuotes = !inQuotes;
|
|
176
|
+
if (char === ',' && !inQuotes) {
|
|
177
|
+
attrParts.push(currentPart.trim());
|
|
178
|
+
currentPart = '';
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
currentPart += char;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (currentPart)
|
|
185
|
+
attrParts.push(currentPart.trim());
|
|
186
|
+
for (const part of attrParts) {
|
|
187
|
+
const m = part.match(/^([\w-]+)\s*(=|~=)\s*(?:(["'])(.*?)\3|(.+))$/);
|
|
188
|
+
if (m) {
|
|
189
|
+
const operator = m[2];
|
|
190
|
+
const value = m[4] !== undefined ? m[4] : m[5];
|
|
191
|
+
attributes[m[1]] = {
|
|
192
|
+
operator,
|
|
193
|
+
value,
|
|
194
|
+
compiled: operator === '~=' ? new RegExp(value) : undefined
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Parse Output
|
|
200
|
+
const outputParts = outputStr.split(':');
|
|
201
|
+
const fresh = outputParts.includes('fresh');
|
|
202
|
+
const mainOutput = outputParts[0];
|
|
203
|
+
const outputMatch = mainOutput.match(/^([a-z0-9]+)?((?:\.[\w-]+)*)(?:\[(.+?)\])?$/);
|
|
204
|
+
if (!outputMatch) {
|
|
205
|
+
throw (0, errorUtils_js_1.getOfficeError)(types_js_1.OfficeErrorType.INVALID_OUTPUT_MAPPING, undefined, mainOutput);
|
|
206
|
+
}
|
|
207
|
+
const tag = outputMatch[1] || 'div';
|
|
208
|
+
const classes = outputMatch[2] ? outputMatch[2].split('.').filter(Boolean) : [];
|
|
209
|
+
const outAttrs = {};
|
|
210
|
+
if (outputMatch[3]) {
|
|
211
|
+
const outAttrParts = outputMatch[3].split(',').map(a => a.trim());
|
|
212
|
+
for (const part of outAttrParts) {
|
|
213
|
+
const m = part.match(/^([\w-]+)\s*=\s*(?:(["'])(.*?)\2|(.+))$/);
|
|
214
|
+
if (m)
|
|
215
|
+
outAttrs[m[1]] = m[3] !== undefined ? m[3] : m[4];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
selector: { nodeType, attributes },
|
|
220
|
+
output: { tag, classes, attributes: outAttrs, fresh }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.StyleMapper = StyleMapper;
|