@stll/docx-core 0.17.2 → 0.18.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 +5 -3
- package/dist/{document-P4LhYoK3.d.ts → document-C4ms2O35.d.ts} +2 -0
- package/dist/docx_kernel_bg.wasm +0 -0
- package/dist/index.d.ts +31 -2
- package/dist/index.js +761 -310
- package/dist/model/document.d.ts +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ A typed OOXML/DOCX document model with parsing, validation, and serialization.
|
|
|
4
4
|
|
|
5
5
|
The package exposes a structured document model (paragraphs, runs, tables,
|
|
6
6
|
styles, section properties) together with the tools to produce and check DOCX
|
|
7
|
-
packages, plus a legal-source compiler that turns a
|
|
8
|
-
model or a finished DOCX file
|
|
7
|
+
packages, plus a legal-source compiler that turns a legal draft (GFM markdown
|
|
8
|
+
plus `@` directives) into that model or a finished DOCX file, and a plain
|
|
9
|
+
markdown reader (`compileMarkdownToContent`) that shares its parser.
|
|
9
10
|
|
|
10
11
|
```ts
|
|
11
12
|
import { compileLegalSourceToDocx, validateDocxPackage } from "@stll/docx-core";
|
|
@@ -30,7 +31,8 @@ bun add @stll/docx-core
|
|
|
30
31
|
|
|
31
32
|
- `.` — the document model types, the legal-source compiler
|
|
32
33
|
(`parseLegalSource`, `compileLegalSourceToDocument`,
|
|
33
|
-
`compileLegalSourceToDocx`, `validateLegalDraft`),
|
|
34
|
+
`compileLegalSourceToDocx`, `validateLegalDraft`), the markdown reader
|
|
35
|
+
(`compileMarkdownToContent`, `sanitizeExternalUrl`), DOCX serialization
|
|
34
36
|
(`serializeDocumentToDocx`), and validation (`validateDocxPackage`,
|
|
35
37
|
`validateDocumentModel`, `assertValidDocumentModel`).
|
|
36
38
|
- `./model` — the document model types only.
|
|
@@ -1153,6 +1153,8 @@ type TextBox = {
|
|
|
1153
1153
|
autoFit?: ShapeTextBody["autoFit"];
|
|
1154
1154
|
/** Horizontal text wrapping inside the box */
|
|
1155
1155
|
textWrap?: ShapeTextBody["textWrap"];
|
|
1156
|
+
/** Vertical text alignment inside the box */
|
|
1157
|
+
verticalAlign?: ShapeTextBody["anchor"];
|
|
1156
1158
|
/** Internal margins */
|
|
1157
1159
|
margins?: {
|
|
1158
1160
|
top?: number;
|
package/dist/docx_kernel_bg.wasm
CHANGED
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Dt as RunContent, Et as Run, Kt as Table, Mt as SectionProperties, N as DocumentBody, Xt as TableRow, a as DocxPackage, b as BlockContent, bt as ParagraphContent, en as TextContent, i as DocxConformanceClass, n as Document, p as Style, qt as TableCell, t as DOCX_CONFORMANCE_CLASSES, w as BreakContent, wt as PositionalTab, yt as Paragraph } from "./document-
|
|
1
|
+
import { Dt as RunContent, Et as Run, Kt as Table, Mt as SectionProperties, N as DocumentBody, Xt as TableRow, a as DocxPackage, b as BlockContent, bt as ParagraphContent, en as TextContent, i as DocxConformanceClass, mn as NumberingDefinitions, n as Document, p as Style, qt as TableCell, t as DOCX_CONFORMANCE_CLASSES, w as BreakContent, wt as PositionalTab, yt as Paragraph } from "./document-C4ms2O35.js";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
//#region src/legal-source/types.d.ts
|
|
4
4
|
type LegalDocumentKind = "agreement" | "letter" | "memo" | "checklist" | "pleading" | "other";
|
|
@@ -24,6 +24,12 @@ type LegalSignatureParty = {
|
|
|
24
24
|
signatory?: string;
|
|
25
25
|
title?: string;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* A parsed draft block. Every `text`, `heading`, paragraph, list item, and
|
|
29
|
+
* table cell string keeps its inline GFM markdown (`**bold**`, `*italic*`,
|
|
30
|
+
* `[link](https://…)`, `` `code` ``) and `[[placeholder]]` markers; the
|
|
31
|
+
* compiler renders them into runs.
|
|
32
|
+
*/
|
|
27
33
|
type LegalDraftBlock = {
|
|
28
34
|
type: "title";
|
|
29
35
|
text: string;
|
|
@@ -114,6 +120,29 @@ declare const validateLegalDraft: (draft: LegalDraft) => LegalDraftDiagnostic[];
|
|
|
114
120
|
//#region src/legal-source/index.d.ts
|
|
115
121
|
declare const compileLegalSourceToDocx: (source: string, options?: LegalSourceCompileOptions) => Promise<LegalSourceDocxCompileResult>;
|
|
116
122
|
//#endregion
|
|
123
|
+
//#region src/markdown/content.d.ts
|
|
124
|
+
/** Block content and the numbering definitions its list paragraphs reference. */
|
|
125
|
+
type MarkdownContent = {
|
|
126
|
+
content: BlockContent[];
|
|
127
|
+
/** Present only when the markdown contained at least one list. */
|
|
128
|
+
numbering?: NumberingDefinitions;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Parse GFM markdown into document blocks plus the numbering its lists need.
|
|
132
|
+
* Synchronous. The caller places the blocks into a `Document` of its own
|
|
133
|
+
* (page geometry, styles, and presets are the host's decision).
|
|
134
|
+
*/
|
|
135
|
+
declare const compileMarkdownToContent: (markdown: string) => MarkdownContent;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/markdown/href.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* Keep only http(s), mailto, and tel URLs, normalised through the URL parser.
|
|
140
|
+
* Anything else (javascript:, data:, relative paths, malformed input) drops to
|
|
141
|
+
* `undefined` so a markdown link degrades to its text instead of carrying an
|
|
142
|
+
* executable target into the document.
|
|
143
|
+
*/
|
|
144
|
+
declare const sanitizeExternalUrl: (rawUrl: string | undefined) => string | undefined;
|
|
145
|
+
//#endregion
|
|
117
146
|
//#region src/serialize/docx.d.ts
|
|
118
147
|
type SerializeDocumentOptions = {
|
|
119
148
|
/** BCP-47 language tag (e.g. "en", "cs", "cs-CZ"); used for footer labels. */
|
|
@@ -150,4 +179,4 @@ declare const validateDocxPackage: (buffer: ArrayBuffer | Uint8Array) => Promise
|
|
|
150
179
|
declare const validateDocumentModel: (document: Document) => ValidateDocumentModelResult;
|
|
151
180
|
declare const assertValidDocumentModel: (document: Document) => void;
|
|
152
181
|
//#endregion
|
|
153
|
-
export { type Autofix, type BlockContent, type BreakContent, type CompiledLegalDocument, DOCX_CONFORMANCE_CLASSES, DOCX_PACKAGE_ISSUE_CODES, type Document, type DocumentBody, type DocxConformanceClass, type DocxPackage, type DocxPackageIssueCode, type LegalDraft, type LegalDraftBlock, type LegalDraftDiagnostic, type LegalSourceCompileOptions, type LegalSourceCompileResult, type LegalSourceDocxCompileResult, type LegalSourceParseResult, type Paragraph, type ParagraphContent, type PositionalTab, type Run, type RunContent, type SectionProperties, type Style, type Table, type TableCell, type TableRow, type TextContent, type ValidateDocumentModelIssue, type ValidateDocumentModelResult, type ValidateDocxPackageResult, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, parseLegalSource, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
|
|
182
|
+
export { type Autofix, type BlockContent, type BreakContent, type CompiledLegalDocument, DOCX_CONFORMANCE_CLASSES, DOCX_PACKAGE_ISSUE_CODES, type Document, type DocumentBody, type DocxConformanceClass, type DocxPackage, type DocxPackageIssueCode, type LegalDraft, type LegalDraftBlock, type LegalDraftDiagnostic, type LegalSourceCompileOptions, type LegalSourceCompileResult, type LegalSourceDocxCompileResult, type LegalSourceParseResult, type MarkdownContent, type Paragraph, type ParagraphContent, type PositionalTab, type Run, type RunContent, type SectionProperties, type Style, type Table, type TableCell, type TableRow, type TextContent, type ValidateDocumentModelIssue, type ValidateDocumentModelResult, type ValidateDocxPackageResult, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, compileMarkdownToContent, parseLegalSource, sanitizeExternalUrl, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { i as isOoxmlSymbolCharacter, t as DOCX_CONFORMANCE_CLASSES } from "./do
|
|
|
2
2
|
import JSZip from "jszip";
|
|
3
3
|
import { panic } from "better-result";
|
|
4
4
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
5
|
+
import { Marked } from "marked";
|
|
5
6
|
//#region src/serialize/xml.ts
|
|
6
7
|
const ILLEGAL_XML_CHARS_RE = /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
|
|
7
8
|
const stripIllegalXmlChars = (value) => value.replace(ILLEGAL_XML_CHARS_RE, "");
|
|
@@ -828,12 +829,251 @@ const validateCounterPairs = (starts, ends, { label, startName, endName, ctx, se
|
|
|
828
829
|
}
|
|
829
830
|
};
|
|
830
831
|
//#endregion
|
|
832
|
+
//#region src/markdown/href.ts
|
|
833
|
+
const ALLOWED_URL_PROTOCOLS = /* @__PURE__ */ new Set([
|
|
834
|
+
"http:",
|
|
835
|
+
"https:",
|
|
836
|
+
"mailto:",
|
|
837
|
+
"tel:"
|
|
838
|
+
]);
|
|
839
|
+
/**
|
|
840
|
+
* Keep only http(s), mailto, and tel URLs, normalised through the URL parser.
|
|
841
|
+
* Anything else (javascript:, data:, relative paths, malformed input) drops to
|
|
842
|
+
* `undefined` so a markdown link degrades to its text instead of carrying an
|
|
843
|
+
* executable target into the document.
|
|
844
|
+
*/
|
|
845
|
+
const sanitizeExternalUrl = (rawUrl) => {
|
|
846
|
+
if (!rawUrl) return;
|
|
847
|
+
const trimmed = rawUrl.trim();
|
|
848
|
+
if (!trimmed) return;
|
|
849
|
+
const parsed = parseUrl(trimmed);
|
|
850
|
+
if (!parsed || !ALLOWED_URL_PROTOCOLS.has(parsed.protocol)) return;
|
|
851
|
+
if ((parsed.protocol === "mailto:" || parsed.protocol === "tel:") && parsed.pathname.trim() === "") return;
|
|
852
|
+
return parsed.href;
|
|
853
|
+
};
|
|
854
|
+
const parseUrl = (value) => {
|
|
855
|
+
try {
|
|
856
|
+
return new URL(value);
|
|
857
|
+
} catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
const hasUnsafeAnchorCharacter = (anchor) => {
|
|
862
|
+
for (const char of anchor) {
|
|
863
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
864
|
+
if (codePoint <= 32 || codePoint === 127 || char.trim() === "") return true;
|
|
865
|
+
}
|
|
866
|
+
return false;
|
|
867
|
+
};
|
|
868
|
+
/** A `#anchor` stays a document-internal target; anything else must pass {@link sanitizeExternalUrl}. */
|
|
869
|
+
const sanitizeMarkdownHref = (rawHref) => {
|
|
870
|
+
const trimmed = rawHref.trim();
|
|
871
|
+
if (!trimmed) return;
|
|
872
|
+
if (trimmed.startsWith("#")) {
|
|
873
|
+
const anchor = trimmed.slice(1);
|
|
874
|
+
if (!anchor || hasUnsafeAnchorCharacter(anchor)) return;
|
|
875
|
+
return `#${anchor}`;
|
|
876
|
+
}
|
|
877
|
+
return sanitizeExternalUrl(trimmed);
|
|
878
|
+
};
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/markdown/lexer.ts
|
|
881
|
+
/**
|
|
882
|
+
* One `marked` configuration for every markdown surface in docx-core. Plain
|
|
883
|
+
* markdown (`compileMarkdownToContent`) and the legal-source compiler read the
|
|
884
|
+
* same GFM token stream; the legal profile adds a block-level extension that
|
|
885
|
+
* turns an `@directive` line into its own token so the line never merges into
|
|
886
|
+
* a neighbouring paragraph.
|
|
887
|
+
*/
|
|
888
|
+
const LEGAL_DIRECTIVE_TOKEN_TYPE = "legalDirective";
|
|
889
|
+
const DIRECTIVE_LINE_PATTERN = /^[ \t]*@(?<name>[A-Za-z][A-Za-z0-9_-]*)(?<argument>[^\n]*)(?:\n|$)/u;
|
|
890
|
+
const DIRECTIVE_LINE_START_PATTERN = /^[ \t]*@[A-Za-z]/mu;
|
|
891
|
+
const legalDirectiveExtension = {
|
|
892
|
+
name: LEGAL_DIRECTIVE_TOKEN_TYPE,
|
|
893
|
+
level: "block",
|
|
894
|
+
start: (src) => {
|
|
895
|
+
const index = src.search(DIRECTIVE_LINE_START_PATTERN);
|
|
896
|
+
return index === -1 ? void 0 : index;
|
|
897
|
+
},
|
|
898
|
+
tokenizer: (src) => {
|
|
899
|
+
const match = DIRECTIVE_LINE_PATTERN.exec(src);
|
|
900
|
+
const name = match?.groups?.["name"];
|
|
901
|
+
if (!match || name === void 0) return;
|
|
902
|
+
return {
|
|
903
|
+
type: LEGAL_DIRECTIVE_TOKEN_TYPE,
|
|
904
|
+
raw: match[0],
|
|
905
|
+
directive: `@${name.toLowerCase()}`,
|
|
906
|
+
argument: (match.groups?.["argument"] ?? "").trim()
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
const plainMarkdown = new Marked({ gfm: true });
|
|
911
|
+
const legalMarkdown = new Marked({
|
|
912
|
+
gfm: true,
|
|
913
|
+
extensions: [legalDirectiveExtension]
|
|
914
|
+
});
|
|
915
|
+
/** GFM block tokens of a markdown document. */
|
|
916
|
+
const lexMarkdown = (source) => plainMarkdown.lexer(source);
|
|
917
|
+
/**
|
|
918
|
+
* A directive line always starts a block. marked's paragraph tokenizer asks
|
|
919
|
+
* block extensions where the next block starts, but its list and blockquote
|
|
920
|
+
* tokenizers treat any following non-blank line as lazy continuation, so
|
|
921
|
+
* `- item\n@clause Next` would swallow the directive. Inserting one blank
|
|
922
|
+
* line before every directive that lacks one gives every tokenizer the same
|
|
923
|
+
* boundary; the returned map keeps diagnostics on the author's line numbers.
|
|
924
|
+
*/
|
|
925
|
+
const separateDirectiveLines = (source) => {
|
|
926
|
+
const lines = source.split("\n");
|
|
927
|
+
const output = [];
|
|
928
|
+
const insertedBefore = [];
|
|
929
|
+
let inserted = 0;
|
|
930
|
+
for (const [index, line] of lines.entries()) {
|
|
931
|
+
const previous = output.at(-1);
|
|
932
|
+
if (index > 0 && DIRECTIVE_LINE_START_PATTERN.test(line) && previous !== void 0 && previous.trim() !== "") {
|
|
933
|
+
output.push("");
|
|
934
|
+
insertedBefore.push(inserted);
|
|
935
|
+
inserted += 1;
|
|
936
|
+
}
|
|
937
|
+
output.push(line);
|
|
938
|
+
insertedBefore.push(inserted);
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
text: output.join("\n"),
|
|
942
|
+
originalLineOf: (line) => line - (insertedBefore.at(line - 1) ?? inserted)
|
|
943
|
+
};
|
|
944
|
+
};
|
|
945
|
+
/** GFM block tokens of a legal draft, with `@directive` lines as {@link LegalDirectiveToken}s. */
|
|
946
|
+
const lexLegalSource = (source) => {
|
|
947
|
+
const prepared = separateDirectiveLines(source);
|
|
948
|
+
return {
|
|
949
|
+
tokens: legalMarkdown.lexer(prepared.text),
|
|
950
|
+
originalLineOf: prepared.originalLineOf
|
|
951
|
+
};
|
|
952
|
+
};
|
|
953
|
+
/** Inline tokens (emphasis, code spans, links, breaks) of one paragraph's text. */
|
|
954
|
+
const lexInlineMarkdown = (text) => plainMarkdown.Lexer.lexInline(text, plainMarkdown.defaults);
|
|
955
|
+
const isLegalDirectiveToken = (token) => token.type === LEGAL_DIRECTIVE_TOKEN_TYPE;
|
|
956
|
+
const isTokenType = (token, type) => token.type === type;
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/markdown/inline.ts
|
|
959
|
+
const MONO_FONT = {
|
|
960
|
+
ascii: "Courier New",
|
|
961
|
+
hAnsi: "Courier New"
|
|
962
|
+
};
|
|
963
|
+
/**
|
|
964
|
+
* One run. A Word run cannot carry a raw newline (the layout engine renders
|
|
965
|
+
* such lines on top of each other), so "\n" becomes an explicit break node.
|
|
966
|
+
*/
|
|
967
|
+
const textRun = (text, format = {}) => {
|
|
968
|
+
const formatting = {
|
|
969
|
+
...format.bold ? { bold: true } : {},
|
|
970
|
+
...format.italic ? { italic: true } : {},
|
|
971
|
+
...format.strike ? { strike: true } : {},
|
|
972
|
+
...format.mono ? { fontFamily: MONO_FONT } : {},
|
|
973
|
+
...format.highlight ? { highlight: format.highlight } : {}
|
|
974
|
+
};
|
|
975
|
+
const content = [];
|
|
976
|
+
for (const [index, segment] of text.split("\n").entries()) {
|
|
977
|
+
if (index > 0) content.push({ type: "break" });
|
|
978
|
+
if (segment.length > 0) content.push({
|
|
979
|
+
type: "text",
|
|
980
|
+
text: segment,
|
|
981
|
+
preserveSpace: true
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
if (content.length === 0) content.push({
|
|
985
|
+
type: "text",
|
|
986
|
+
text: "",
|
|
987
|
+
preserveSpace: true
|
|
988
|
+
});
|
|
989
|
+
return {
|
|
990
|
+
type: "run",
|
|
991
|
+
formatting,
|
|
992
|
+
content
|
|
993
|
+
};
|
|
994
|
+
};
|
|
995
|
+
const PLACEHOLDER_PATTERN = /\[\[(?<inner>[^\][]+?)\]\]/gu;
|
|
996
|
+
/**
|
|
997
|
+
* Literal text (no markdown reading) with `[[…]]` placeholders highlighted:
|
|
998
|
+
* for fields that are data rather than prose, such as signature parties.
|
|
999
|
+
*/
|
|
1000
|
+
const plainTextRuns = (text, format = {}) => placeholderRuns(text, format);
|
|
1001
|
+
const placeholderRuns = (text, format) => {
|
|
1002
|
+
if (!text.includes("[[")) return [textRun(text, format)];
|
|
1003
|
+
const runs = [];
|
|
1004
|
+
let cursor = 0;
|
|
1005
|
+
for (const match of text.matchAll(PLACEHOLDER_PATTERN)) {
|
|
1006
|
+
const start = match.index;
|
|
1007
|
+
if (start > cursor) runs.push(textRun(text.slice(cursor, start), format));
|
|
1008
|
+
runs.push(textRun(match.groups?.["inner"] ?? "", {
|
|
1009
|
+
...format,
|
|
1010
|
+
highlight: "yellow"
|
|
1011
|
+
}));
|
|
1012
|
+
cursor = start + match[0].length;
|
|
1013
|
+
}
|
|
1014
|
+
if (cursor < text.length) runs.push(textRun(text.slice(cursor), format));
|
|
1015
|
+
return runs.length > 0 ? runs : [textRun(text, format)];
|
|
1016
|
+
};
|
|
1017
|
+
const plainRuns = (text, format, placeholders) => placeholders ? placeholderRuns(text, format) : [textRun(text, format)];
|
|
1018
|
+
const tokensToRuns = (tokens, fallback, format, context) => {
|
|
1019
|
+
if (!tokens || tokens.length === 0) return plainRuns(fallback, format, context.placeholders);
|
|
1020
|
+
const runs = [];
|
|
1021
|
+
for (const token of tokens) if (isTokenType(token, "strong")) runs.push(...tokensToRuns(token.tokens, token.text, {
|
|
1022
|
+
...format,
|
|
1023
|
+
bold: true
|
|
1024
|
+
}, context));
|
|
1025
|
+
else if (isTokenType(token, "em")) runs.push(...tokensToRuns(token.tokens, token.text, {
|
|
1026
|
+
...format,
|
|
1027
|
+
italic: true
|
|
1028
|
+
}, context));
|
|
1029
|
+
else if (isTokenType(token, "del")) runs.push(...tokensToRuns(token.tokens, token.text, {
|
|
1030
|
+
...format,
|
|
1031
|
+
strike: true
|
|
1032
|
+
}, context));
|
|
1033
|
+
else if (isTokenType(token, "codespan")) runs.push(textRun(token.text, {
|
|
1034
|
+
...format,
|
|
1035
|
+
mono: true
|
|
1036
|
+
}));
|
|
1037
|
+
else if (isTokenType(token, "link")) runs.push(...linkRuns(token.tokens, token.text, token.href, format, context));
|
|
1038
|
+
else if (isTokenType(token, "paragraph")) runs.push(...tokensToRuns(token.tokens, token.text, format, context));
|
|
1039
|
+
else if (token.type === "br") runs.push({
|
|
1040
|
+
type: "run",
|
|
1041
|
+
content: [{ type: "break" }]
|
|
1042
|
+
});
|
|
1043
|
+
else if (token.type === "space") {
|
|
1044
|
+
if (runs.length > 0 && token.raw.includes("\n")) runs.push(textRun("\n", format));
|
|
1045
|
+
} else if (isTokenType(token, "text")) {
|
|
1046
|
+
const nested = token.tokens;
|
|
1047
|
+
if (nested && nested.length > 0) runs.push(...tokensToRuns(nested, token.text, format, context));
|
|
1048
|
+
else runs.push(...plainRuns(token.text, format, context.placeholders));
|
|
1049
|
+
} else if ("text" in token && typeof token.text === "string") runs.push(...plainRuns(token.text, format, context.placeholders));
|
|
1050
|
+
return runs.length > 0 ? runs : plainRuns(fallback, format, context.placeholders);
|
|
1051
|
+
};
|
|
1052
|
+
const linkRuns = (tokens, text, rawHref, format, context) => {
|
|
1053
|
+
const children = tokensToRuns(tokens, text, format, context).filter((child) => child.type === "run");
|
|
1054
|
+
const linkChildren = children.length > 0 ? children : [textRun(text, format)];
|
|
1055
|
+
const href = sanitizeMarkdownHref(rawHref);
|
|
1056
|
+
if (!href) return linkChildren;
|
|
1057
|
+
const anchor = href.startsWith("#") ? href.slice(1) : void 0;
|
|
1058
|
+
return [{
|
|
1059
|
+
type: "hyperlink",
|
|
1060
|
+
href,
|
|
1061
|
+
...anchor ? { anchor } : {},
|
|
1062
|
+
children: linkChildren
|
|
1063
|
+
}];
|
|
1064
|
+
};
|
|
1065
|
+
/** Render already-lexed inline tokens; `fallback` is the source text when the tokens are empty. */
|
|
1066
|
+
const inlineTokensToRuns = (tokens, fallback, options = {}) => tokensToRuns(tokens, fallback, options.base ?? {}, { placeholders: options.placeholders ?? false });
|
|
1067
|
+
/** Lex and render one paragraph's inline markdown. */
|
|
1068
|
+
const inlineMarkdownToRuns = (text, options = {}) => inlineTokensToRuns(lexInlineMarkdown(text), text, options);
|
|
1069
|
+
//#endregion
|
|
831
1070
|
//#region src/legal-source/parser.ts
|
|
832
1071
|
const DEFAULT_KIND = "agreement";
|
|
833
1072
|
const DEFAULT_LOCALE = "en-GB";
|
|
834
1073
|
const DEFAULT_NUMBERING = "legal";
|
|
835
1074
|
const DEFAULT_PAGE_SIZE = "A4";
|
|
836
1075
|
const DEFAULT_ORIENTATION = "portrait";
|
|
1076
|
+
const MAX_CLAUSE_LEVEL = 6;
|
|
837
1077
|
const DIRECTIVE_ALIASES = {
|
|
838
1078
|
"@annex": "@schedule",
|
|
839
1079
|
"@appendix": "@schedule",
|
|
@@ -844,6 +1084,7 @@ const DIRECTIVE_ALIASES = {
|
|
|
844
1084
|
"@signature": "@signatures",
|
|
845
1085
|
"@subsection": "@subclause"
|
|
846
1086
|
};
|
|
1087
|
+
const CLOSING_DIRECTIVE_PATTERN = /^@end[a-z]*$/u;
|
|
847
1088
|
const DIRECTIVES = /* @__PURE__ */ new Set([
|
|
848
1089
|
"@doc",
|
|
849
1090
|
"@title",
|
|
@@ -857,194 +1098,337 @@ const DIRECTIVES = /* @__PURE__ */ new Set([
|
|
|
857
1098
|
"@signatures",
|
|
858
1099
|
"@pagebreak"
|
|
859
1100
|
]);
|
|
1101
|
+
/**
|
|
1102
|
+
* Walks the top-level token stream. Line numbers come from the newlines in
|
|
1103
|
+
* the consumed tokens' `raw` text, which marked keeps contiguous with the
|
|
1104
|
+
* source, so diagnostics point at the directive line that produced them.
|
|
1105
|
+
*/
|
|
1106
|
+
var TokenCursor = class {
|
|
1107
|
+
tokens;
|
|
1108
|
+
originalLineOf;
|
|
1109
|
+
index = 0;
|
|
1110
|
+
lineNumber = 1;
|
|
1111
|
+
constructor({ tokens, originalLineOf }) {
|
|
1112
|
+
this.tokens = tokens;
|
|
1113
|
+
this.originalLineOf = originalLineOf;
|
|
1114
|
+
}
|
|
1115
|
+
get current() {
|
|
1116
|
+
return this.tokens.at(this.index);
|
|
1117
|
+
}
|
|
1118
|
+
/** The author's line number for the current token. */
|
|
1119
|
+
get line() {
|
|
1120
|
+
return this.originalLineOf(this.lineNumber);
|
|
1121
|
+
}
|
|
1122
|
+
/** Index of the current token; lets a caller tell whether a helper consumed anything. */
|
|
1123
|
+
get position() {
|
|
1124
|
+
return this.index;
|
|
1125
|
+
}
|
|
1126
|
+
advance() {
|
|
1127
|
+
const token = this.current;
|
|
1128
|
+
if (token === void 0) return;
|
|
1129
|
+
this.index += 1;
|
|
1130
|
+
this.lineNumber += countNewlines(token.raw);
|
|
1131
|
+
return token;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const countNewlines = (text) => {
|
|
1135
|
+
let count = 0;
|
|
1136
|
+
for (const char of text) if (char === "\n") count += 1;
|
|
1137
|
+
return count;
|
|
1138
|
+
};
|
|
1139
|
+
/** A token that starts a new structural block, so no body may run past it. */
|
|
1140
|
+
const startsStructure = (token) => isLegalDirectiveToken(token) || isTokenType(token, "heading");
|
|
860
1141
|
const parseLegalSource = (source, options = {}) => {
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1142
|
+
const state = {
|
|
1143
|
+
blocks: [],
|
|
1144
|
+
diagnostics: [],
|
|
1145
|
+
fixes: [],
|
|
1146
|
+
meta: {
|
|
1147
|
+
kind: DEFAULT_KIND,
|
|
1148
|
+
locale: DEFAULT_LOCALE,
|
|
1149
|
+
numbering: DEFAULT_NUMBERING,
|
|
1150
|
+
page: {
|
|
1151
|
+
size: DEFAULT_PAGE_SIZE,
|
|
1152
|
+
orientation: DEFAULT_ORIENTATION
|
|
1153
|
+
},
|
|
1154
|
+
title: null
|
|
871
1155
|
},
|
|
872
|
-
|
|
873
|
-
};
|
|
874
|
-
let pending = null;
|
|
875
|
-
const pushPending = () => {
|
|
876
|
-
if (!pending) return;
|
|
877
|
-
const block = pendingToBlock(pending, diagnostics, fixes);
|
|
878
|
-
if (block) {
|
|
879
|
-
blocks.push(block);
|
|
880
|
-
if (block.type === "title") meta.title = block.text;
|
|
881
|
-
}
|
|
882
|
-
pending = null;
|
|
1156
|
+
cursor: new TokenCursor(lexLegalSource(source))
|
|
883
1157
|
};
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const line =
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1158
|
+
for (;;) {
|
|
1159
|
+
const token = state.cursor.current;
|
|
1160
|
+
if (token === void 0) break;
|
|
1161
|
+
const line = state.cursor.line;
|
|
1162
|
+
if (isLegalDirectiveToken(token)) {
|
|
1163
|
+
state.cursor.advance();
|
|
1164
|
+
parseDirective(token, line, state);
|
|
891
1165
|
continue;
|
|
892
1166
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const { depth, heading } = markdownHeading;
|
|
897
|
-
if (depth === 1) {
|
|
898
|
-
pending = {
|
|
899
|
-
type: "title",
|
|
900
|
-
line: lineNumber,
|
|
901
|
-
heading,
|
|
902
|
-
lines: []
|
|
903
|
-
};
|
|
904
|
-
pushPending();
|
|
905
|
-
} else pending = {
|
|
906
|
-
type: "clause",
|
|
907
|
-
line: lineNumber,
|
|
908
|
-
level: Math.min(depth - 1, 6),
|
|
909
|
-
heading,
|
|
910
|
-
lines: []
|
|
911
|
-
};
|
|
912
|
-
fixes.push({
|
|
913
|
-
code: "markdown-heading-normalized",
|
|
914
|
-
message: "Converted a Markdown heading into a legal directive.",
|
|
915
|
-
line: lineNumber
|
|
916
|
-
});
|
|
1167
|
+
if (isTokenType(token, "heading")) {
|
|
1168
|
+
state.cursor.advance();
|
|
1169
|
+
parseMarkdownHeading(token, line, state);
|
|
917
1170
|
continue;
|
|
918
1171
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1172
|
+
parseBareMarkdown(state);
|
|
1173
|
+
}
|
|
1174
|
+
if (!state.meta.title) {
|
|
1175
|
+
const firstTitle = state.blocks.find((block) => block.type === "title");
|
|
1176
|
+
state.meta.title = firstTitle?.type === "title" ? firstTitle.text : options.titleFallback ?? "Untitled document";
|
|
1177
|
+
}
|
|
1178
|
+
const draft = {
|
|
1179
|
+
meta: state.meta,
|
|
1180
|
+
blocks: state.blocks
|
|
1181
|
+
};
|
|
1182
|
+
return applyDocumentAutofixes({
|
|
1183
|
+
diagnostics: state.diagnostics,
|
|
1184
|
+
draft,
|
|
1185
|
+
fixes: state.fixes
|
|
1186
|
+
});
|
|
1187
|
+
};
|
|
1188
|
+
const pushBlock = (state, block) => {
|
|
1189
|
+
if (!block) return;
|
|
1190
|
+
state.blocks.push(block);
|
|
1191
|
+
if (block.type === "title") state.meta.title = block.text;
|
|
1192
|
+
};
|
|
1193
|
+
const parseDirective = (token, line, state) => {
|
|
1194
|
+
const { diagnostics, fixes, meta } = state;
|
|
1195
|
+
const rawDirective = token.directive;
|
|
1196
|
+
const directive = DIRECTIVE_ALIASES[rawDirective] ?? rawDirective;
|
|
1197
|
+
const { argument } = token;
|
|
1198
|
+
if (CLOSING_DIRECTIVE_PATTERN.test(directive)) {
|
|
1199
|
+
fixes.push({
|
|
1200
|
+
code: "closing-directive-ignored",
|
|
1201
|
+
message: `Ignored ${rawDirective}: blocks end where the next directive starts.`,
|
|
1202
|
+
line
|
|
1203
|
+
});
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (!DIRECTIVES.has(directive)) {
|
|
1207
|
+
const spelled = token.raw.trim().split(/\s+/u).at(0) ?? rawDirective;
|
|
1208
|
+
diagnostics.push({
|
|
1209
|
+
code: "unknown-directive",
|
|
1210
|
+
message: `Unknown legal directive "${spelled}".`,
|
|
1211
|
+
severity: "error",
|
|
1212
|
+
line
|
|
1213
|
+
});
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (directive !== rawDirective) fixes.push({
|
|
1217
|
+
code: "directive-alias-normalized",
|
|
1218
|
+
message: `Normalized ${rawDirective} to ${directive}.`,
|
|
1219
|
+
line
|
|
1220
|
+
});
|
|
1221
|
+
switch (directive) {
|
|
1222
|
+
case "@doc":
|
|
1223
|
+
parseDocDirective(argument, meta, diagnostics, line);
|
|
1224
|
+
return;
|
|
1225
|
+
case "@title":
|
|
1226
|
+
pushBlock(state, argument ? {
|
|
1227
|
+
type: "title",
|
|
1228
|
+
text: argument
|
|
1229
|
+
} : null);
|
|
1230
|
+
return;
|
|
1231
|
+
case "@recital":
|
|
1232
|
+
pushBlock(state, {
|
|
1233
|
+
type: "recital",
|
|
1234
|
+
paragraphs: takeParagraphs(state)
|
|
937
1235
|
});
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
line: lineNumber,
|
|
964
|
-
level: 1,
|
|
965
|
-
heading: argument,
|
|
966
|
-
lines: []
|
|
967
|
-
};
|
|
968
|
-
break;
|
|
969
|
-
case "@subclause":
|
|
970
|
-
pending = {
|
|
971
|
-
type: "clause",
|
|
972
|
-
line: lineNumber,
|
|
973
|
-
level: 2,
|
|
974
|
-
heading: argument,
|
|
975
|
-
lines: []
|
|
976
|
-
};
|
|
977
|
-
break;
|
|
978
|
-
case "@paragraph":
|
|
979
|
-
pending = {
|
|
980
|
-
type: "paragraph",
|
|
981
|
-
line: lineNumber,
|
|
982
|
-
heading: argument,
|
|
983
|
-
lines: []
|
|
984
|
-
};
|
|
985
|
-
break;
|
|
986
|
-
case "@list":
|
|
987
|
-
pending = {
|
|
988
|
-
type: "list",
|
|
989
|
-
line: lineNumber,
|
|
990
|
-
ordered: /\bordered\b/iu.test(argument),
|
|
991
|
-
heading: argument,
|
|
992
|
-
lines: []
|
|
993
|
-
};
|
|
994
|
-
break;
|
|
995
|
-
case "@table":
|
|
996
|
-
pending = {
|
|
997
|
-
type: "table",
|
|
998
|
-
line: lineNumber,
|
|
999
|
-
heading: argument,
|
|
1000
|
-
lines: []
|
|
1001
|
-
};
|
|
1002
|
-
break;
|
|
1003
|
-
case "@schedule":
|
|
1004
|
-
pending = {
|
|
1005
|
-
type: "schedule",
|
|
1006
|
-
line: lineNumber,
|
|
1007
|
-
heading: argument,
|
|
1008
|
-
lines: []
|
|
1009
|
-
};
|
|
1010
|
-
break;
|
|
1011
|
-
case "@signatures":
|
|
1012
|
-
pending = {
|
|
1013
|
-
type: "signatures",
|
|
1014
|
-
line: lineNumber,
|
|
1015
|
-
heading: argument,
|
|
1016
|
-
lines: []
|
|
1017
|
-
};
|
|
1018
|
-
break;
|
|
1019
|
-
case "@pagebreak":
|
|
1020
|
-
blocks.push({ type: "pageBreak" });
|
|
1021
|
-
break;
|
|
1022
|
-
default: break;
|
|
1023
|
-
}
|
|
1024
|
-
continue;
|
|
1236
|
+
return;
|
|
1237
|
+
case "@clause":
|
|
1238
|
+
pushBlock(state, clauseBlock(1, argument, line, state));
|
|
1239
|
+
return;
|
|
1240
|
+
case "@subclause":
|
|
1241
|
+
pushBlock(state, clauseBlock(2, argument, line, state));
|
|
1242
|
+
return;
|
|
1243
|
+
case "@paragraph":
|
|
1244
|
+
pushBlock(state, {
|
|
1245
|
+
type: "paragraph",
|
|
1246
|
+
paragraphs: takeParagraphs(state)
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
case "@list": {
|
|
1250
|
+
const ordered = /\bordered\b/iu.test(argument);
|
|
1251
|
+
const items = takeRawLines(state).flatMap((rawLine) => {
|
|
1252
|
+
const stripped = stripListMarker(rawLine, ordered);
|
|
1253
|
+
return stripped ? [stripped] : [];
|
|
1254
|
+
});
|
|
1255
|
+
pushBlock(state, {
|
|
1256
|
+
type: "list",
|
|
1257
|
+
ordered,
|
|
1258
|
+
items
|
|
1259
|
+
});
|
|
1260
|
+
return;
|
|
1025
1261
|
}
|
|
1026
|
-
|
|
1262
|
+
case "@table":
|
|
1263
|
+
pushBlock(state, parseTableBlock(takeRawLines(state), line, diagnostics, fixes));
|
|
1264
|
+
return;
|
|
1265
|
+
case "@schedule":
|
|
1266
|
+
pushBlock(state, {
|
|
1267
|
+
type: "schedule",
|
|
1268
|
+
heading: stripManualNumbering(argument, line, fixes),
|
|
1269
|
+
paragraphs: takeParagraphs(state)
|
|
1270
|
+
});
|
|
1271
|
+
return;
|
|
1272
|
+
case "@signatures":
|
|
1273
|
+
pushBlock(state, {
|
|
1274
|
+
type: "signatures",
|
|
1275
|
+
parties: parseSignatureParties(takeRawLines(state), argument)
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
case "@pagebreak":
|
|
1279
|
+
pushBlock(state, { type: "pageBreak" });
|
|
1280
|
+
return;
|
|
1281
|
+
default: return;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
const clauseBlock = (level, rawHeading, line, state) => {
|
|
1285
|
+
const heading = stripManualNumbering(rawHeading, line, state.fixes);
|
|
1286
|
+
const paragraphs = takeParagraphs(state);
|
|
1287
|
+
if (!heading) {
|
|
1288
|
+
state.fixes.push({
|
|
1289
|
+
code: "headingless-clause-downgraded",
|
|
1290
|
+
message: "Converted a headingless @clause into a paragraph block.",
|
|
1291
|
+
line
|
|
1292
|
+
});
|
|
1293
|
+
return {
|
|
1027
1294
|
type: "paragraph",
|
|
1028
|
-
|
|
1029
|
-
heading: "",
|
|
1030
|
-
lines: []
|
|
1295
|
+
paragraphs
|
|
1031
1296
|
};
|
|
1032
|
-
pending.lines.push(line);
|
|
1033
1297
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1298
|
+
return {
|
|
1299
|
+
type: "clause",
|
|
1300
|
+
level,
|
|
1301
|
+
heading,
|
|
1302
|
+
paragraphs
|
|
1303
|
+
};
|
|
1304
|
+
};
|
|
1305
|
+
const parseMarkdownHeading = (token, line, state) => {
|
|
1306
|
+
const heading = token.text.trim();
|
|
1307
|
+
state.fixes.push({
|
|
1308
|
+
code: "markdown-heading-normalized",
|
|
1309
|
+
message: "Converted a Markdown heading into a legal directive.",
|
|
1310
|
+
line
|
|
1311
|
+
});
|
|
1312
|
+
if (token.depth === 1) {
|
|
1313
|
+
pushBlock(state, heading ? {
|
|
1314
|
+
type: "title",
|
|
1315
|
+
text: heading
|
|
1316
|
+
} : null);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
pushBlock(state, clauseBlock(Math.min(token.depth - 1, MAX_CLAUSE_LEVEL), heading, line, state));
|
|
1320
|
+
};
|
|
1321
|
+
/**
|
|
1322
|
+
* Markdown outside any directive. Consecutive prose stays one paragraph
|
|
1323
|
+
* block (several paragraphs), while a list or table is its own block so
|
|
1324
|
+
* markdown lists get real numbering instead of literal `-` markers.
|
|
1325
|
+
*/
|
|
1326
|
+
const parseBareMarkdown = (state) => {
|
|
1327
|
+
const token = state.cursor.current;
|
|
1328
|
+
if (token === void 0) return;
|
|
1329
|
+
if (isTokenType(token, "list")) {
|
|
1330
|
+
state.cursor.advance();
|
|
1331
|
+
pushBlock(state, {
|
|
1332
|
+
type: "list",
|
|
1333
|
+
ordered: token.ordered,
|
|
1334
|
+
items: flattenListItems(token)
|
|
1335
|
+
});
|
|
1336
|
+
return;
|
|
1038
1337
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1338
|
+
if (isTokenType(token, "table")) {
|
|
1339
|
+
state.cursor.advance();
|
|
1340
|
+
pushBlock(state, {
|
|
1341
|
+
type: "table",
|
|
1342
|
+
table: {
|
|
1343
|
+
headers: token.header.map((cell) => cellText(cell)),
|
|
1344
|
+
rows: token.rows.map((row) => row.map((cell) => cellText(cell)))
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const before = state.cursor.position;
|
|
1350
|
+
const paragraphs = takeParagraphs(state);
|
|
1351
|
+
if (paragraphs.length > 0) {
|
|
1352
|
+
pushBlock(state, {
|
|
1353
|
+
type: "paragraph",
|
|
1354
|
+
paragraphs
|
|
1355
|
+
});
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (state.cursor.position === before) state.cursor.advance();
|
|
1359
|
+
};
|
|
1360
|
+
/**
|
|
1361
|
+
* Consecutive prose tokens as paragraph strings: paragraphs (soft line
|
|
1362
|
+
* breaks collapsed to spaces), blockquotes, code blocks, and raw HTML. Stops
|
|
1363
|
+
* at the next directive, heading, list, or table so those keep their order.
|
|
1364
|
+
*/
|
|
1365
|
+
const takeParagraphs = (state) => {
|
|
1366
|
+
const paragraphs = [];
|
|
1367
|
+
for (;;) {
|
|
1368
|
+
const token = state.cursor.current;
|
|
1369
|
+
if (token === void 0 || startsStructure(token)) return paragraphs;
|
|
1370
|
+
if (isTokenType(token, "list") || isTokenType(token, "table")) return paragraphs;
|
|
1371
|
+
const prose = proseParagraphs(token);
|
|
1372
|
+
if (prose === null) return paragraphs;
|
|
1373
|
+
state.cursor.advance();
|
|
1374
|
+
paragraphs.push(...prose);
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
/** The paragraph strings of one prose token, or `null` when the token is not prose. */
|
|
1378
|
+
const proseParagraphs = (token) => {
|
|
1379
|
+
if (token.type === "space" || token.type === "hr") return [];
|
|
1380
|
+
if (isTokenType(token, "paragraph") || isTokenType(token, "text")) {
|
|
1381
|
+
const text = collapseSoftBreaks(token.text);
|
|
1382
|
+
return text ? [text] : [];
|
|
1383
|
+
}
|
|
1384
|
+
if (isTokenType(token, "blockquote")) return token.tokens.flatMap((inner) => proseParagraphs(inner) ?? []);
|
|
1385
|
+
if (isTokenType(token, "code")) return token.text.split("\n").flatMap((codeLine) => {
|
|
1386
|
+
const trimmed = codeLine.trim();
|
|
1387
|
+
return trimmed ? [escapeInlineMarkdown(trimmed)] : [];
|
|
1046
1388
|
});
|
|
1389
|
+
if (isTokenType(token, "html")) {
|
|
1390
|
+
const text = collapseSoftBreaks(token.text);
|
|
1391
|
+
return text ? [text] : [];
|
|
1392
|
+
}
|
|
1393
|
+
return null;
|
|
1047
1394
|
};
|
|
1395
|
+
/**
|
|
1396
|
+
* Raw source lines of everything up to the next directive or heading, for
|
|
1397
|
+
* the line-oriented directive bodies (`@list`, `@table`, `@signatures`).
|
|
1398
|
+
*/
|
|
1399
|
+
const takeRawLines = (state) => {
|
|
1400
|
+
let raw = "";
|
|
1401
|
+
for (;;) {
|
|
1402
|
+
const token = state.cursor.current;
|
|
1403
|
+
if (token === void 0 || startsStructure(token)) break;
|
|
1404
|
+
state.cursor.advance();
|
|
1405
|
+
raw += token.raw;
|
|
1406
|
+
}
|
|
1407
|
+
return raw.split("\n").map((rawLine) => rawLine.trimEnd());
|
|
1408
|
+
};
|
|
1409
|
+
const flattenListItems = (list) => {
|
|
1410
|
+
const items = [];
|
|
1411
|
+
for (const item of list.items) {
|
|
1412
|
+
const parts = [];
|
|
1413
|
+
const nested = [];
|
|
1414
|
+
for (const child of item.tokens) {
|
|
1415
|
+
if (isTokenType(child, "list")) {
|
|
1416
|
+
nested.push(child);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
parts.push(...proseParagraphs(child) ?? []);
|
|
1420
|
+
}
|
|
1421
|
+
const text = parts.join(" ").trim();
|
|
1422
|
+
if (text) items.push(text);
|
|
1423
|
+
for (const nestedList of nested) items.push(...flattenListItems(nestedList));
|
|
1424
|
+
}
|
|
1425
|
+
return items;
|
|
1426
|
+
};
|
|
1427
|
+
const cellText = (cell) => collapseSoftBreaks(cell.text);
|
|
1428
|
+
const INLINE_MARKDOWN_SPECIALS = /[\\`*_[\]<>~!]/gu;
|
|
1429
|
+
/** Backslash-escape the inline markdown syntax so the text renders verbatim. */
|
|
1430
|
+
const escapeInlineMarkdown = (text) => text.replaceAll(INLINE_MARKDOWN_SPECIALS, (char) => `\\${char}`);
|
|
1431
|
+
const collapseSoftBreaks = (text) => text.split("\n").map((textLine) => textLine.trim()).filter((textLine) => textLine.length > 0).join(" ");
|
|
1048
1432
|
const parseDocDirective = (argument, meta, diagnostics, line) => {
|
|
1049
1433
|
const attrs = parseAttributes(argument);
|
|
1050
1434
|
const kind = attrs.get("kind");
|
|
@@ -1164,84 +1548,12 @@ const isAsciiAlphaNumeric = (char) => {
|
|
|
1164
1548
|
if (code === void 0) return false;
|
|
1165
1549
|
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
1166
1550
|
};
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
if (depth < 1 || depth > 6 || line.at(depth) !== " ") return null;
|
|
1174
|
-
const heading = line.slice(depth + 1).trim();
|
|
1175
|
-
return heading ? {
|
|
1176
|
-
depth,
|
|
1177
|
-
heading
|
|
1178
|
-
} : null;
|
|
1179
|
-
};
|
|
1180
|
-
const pendingToBlock = (pending, diagnostics, fixes) => {
|
|
1181
|
-
switch (pending.type) {
|
|
1182
|
-
case "title": {
|
|
1183
|
-
const text = pending.heading || paragraphText(pending.lines);
|
|
1184
|
-
if (!text) return null;
|
|
1185
|
-
return {
|
|
1186
|
-
type: "title",
|
|
1187
|
-
text
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
case "recital": return {
|
|
1191
|
-
type: "recital",
|
|
1192
|
-
paragraphs: compactParagraphs(pending.lines)
|
|
1193
|
-
};
|
|
1194
|
-
case "clause": {
|
|
1195
|
-
const heading = stripManualNumbering(pending.heading, pending.line, fixes);
|
|
1196
|
-
if (!heading) {
|
|
1197
|
-
fixes.push({
|
|
1198
|
-
code: "headingless-clause-downgraded",
|
|
1199
|
-
message: "Converted a headingless @clause into a paragraph block.",
|
|
1200
|
-
line: pending.line
|
|
1201
|
-
});
|
|
1202
|
-
return {
|
|
1203
|
-
type: "paragraph",
|
|
1204
|
-
paragraphs: compactParagraphs(pending.lines)
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
return {
|
|
1208
|
-
type: "clause",
|
|
1209
|
-
level: pending.level,
|
|
1210
|
-
heading,
|
|
1211
|
-
paragraphs: compactParagraphs(pending.lines)
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
case "paragraph": return {
|
|
1215
|
-
type: "paragraph",
|
|
1216
|
-
paragraphs: compactParagraphs(pending.lines)
|
|
1217
|
-
};
|
|
1218
|
-
case "list": return {
|
|
1219
|
-
type: "list",
|
|
1220
|
-
ordered: pending.ordered,
|
|
1221
|
-
items: pending.lines.flatMap((line) => {
|
|
1222
|
-
const stripped = stripListMarker(line, pending.ordered);
|
|
1223
|
-
return stripped ? [stripped] : [];
|
|
1224
|
-
})
|
|
1225
|
-
};
|
|
1226
|
-
case "table": return parseTableBlock(pending, diagnostics, fixes);
|
|
1227
|
-
case "schedule": return {
|
|
1228
|
-
type: "schedule",
|
|
1229
|
-
heading: stripManualNumbering(pending.heading, pending.line, fixes),
|
|
1230
|
-
paragraphs: compactParagraphs(pending.lines)
|
|
1231
|
-
};
|
|
1232
|
-
case "signatures": return {
|
|
1233
|
-
type: "signatures",
|
|
1234
|
-
parties: parseSignatureParties(pending.lines, pending.heading)
|
|
1235
|
-
};
|
|
1236
|
-
default: return null;
|
|
1237
|
-
}
|
|
1238
|
-
};
|
|
1239
|
-
const parseTableBlock = (pending, diagnostics, fixes) => {
|
|
1240
|
-
const rows = pending.lines.flatMap((rawLine) => {
|
|
1241
|
-
const line = rawLine.trim();
|
|
1242
|
-
return line.startsWith("|") ? [line] : [];
|
|
1243
|
-
}).flatMap((line) => {
|
|
1244
|
-
const row = parsePipeRow(line);
|
|
1551
|
+
const parseTableBlock = (lines, line, diagnostics, fixes) => {
|
|
1552
|
+
const rows = lines.flatMap((rawLine) => {
|
|
1553
|
+
const trimmed = rawLine.trim();
|
|
1554
|
+
return trimmed.startsWith("|") ? [trimmed] : [];
|
|
1555
|
+
}).flatMap((tableLine) => {
|
|
1556
|
+
const row = parsePipeRow(tableLine);
|
|
1245
1557
|
return row.length > 0 ? [row] : [];
|
|
1246
1558
|
});
|
|
1247
1559
|
const header = rows.at(0) ?? [];
|
|
@@ -1250,7 +1562,7 @@ const parseTableBlock = (pending, diagnostics, fixes) => {
|
|
|
1250
1562
|
fixes.push({
|
|
1251
1563
|
code: "table-row-width-normalized",
|
|
1252
1564
|
message: "Normalized a table row to match the header width.",
|
|
1253
|
-
line
|
|
1565
|
+
line
|
|
1254
1566
|
});
|
|
1255
1567
|
return header.map((_, index) => row.at(index) ?? "");
|
|
1256
1568
|
});
|
|
@@ -1258,7 +1570,7 @@ const parseTableBlock = (pending, diagnostics, fixes) => {
|
|
|
1258
1570
|
code: "missing-table-header",
|
|
1259
1571
|
message: "Table directives must include a pipe-table header row.",
|
|
1260
1572
|
severity: "error",
|
|
1261
|
-
line
|
|
1573
|
+
line
|
|
1262
1574
|
});
|
|
1263
1575
|
return {
|
|
1264
1576
|
type: "table",
|
|
@@ -1380,8 +1692,8 @@ const parseSignatureParties = (lines, heading) => {
|
|
|
1380
1692
|
current = { name };
|
|
1381
1693
|
};
|
|
1382
1694
|
if (heading.trim()) startParty(heading.trim().replace(/^party:\s*/iu, ""));
|
|
1383
|
-
for (const
|
|
1384
|
-
const trimmed =
|
|
1695
|
+
for (const rawLine of lines) {
|
|
1696
|
+
const trimmed = rawLine.trim();
|
|
1385
1697
|
if (!trimmed) continue;
|
|
1386
1698
|
const separatorIndex = trimmed.indexOf(":");
|
|
1387
1699
|
if (separatorIndex === -1) {
|
|
@@ -1414,31 +1726,13 @@ const parseSignatureParties = (lines, heading) => {
|
|
|
1414
1726
|
};
|
|
1415
1727
|
const ORDERED_LIST_MARKER_RE = /^(?:\d+(?:\.\d+)+|\d+[.)])\s+/u;
|
|
1416
1728
|
const MANUAL_NUMBERING_PREFIX_RE = /^(?:\d+(?:\.\d+)+|\d+[.)]|[A-Za-z][.)]|\([a-zivx]+\))\s+/u;
|
|
1417
|
-
const stripListMarker = (
|
|
1418
|
-
const trimmed =
|
|
1729
|
+
const stripListMarker = (rawLine, ordered) => {
|
|
1730
|
+
const trimmed = rawLine.trim();
|
|
1419
1731
|
if (ordered) return trimmed.replace(ORDERED_LIST_MARKER_RE, "");
|
|
1420
1732
|
return trimmed.replace(/^[-*•]\s+/u, "");
|
|
1421
1733
|
};
|
|
1422
|
-
const parsePipeRow = (
|
|
1734
|
+
const parsePipeRow = (tableLine) => tableLine.replace(/^\|/u, "").replace(/\|$/u, "").split("|").map((cell) => cell.trim());
|
|
1423
1735
|
const isMarkdownDividerRow = (row) => row.every((cell) => /^:?-{3,}:?$/u.test(cell));
|
|
1424
|
-
const compactParagraphs = (lines) => {
|
|
1425
|
-
const paragraphs = [];
|
|
1426
|
-
let current = [];
|
|
1427
|
-
for (const line of lines) {
|
|
1428
|
-
const trimmed = line.trim();
|
|
1429
|
-
if (!trimmed) {
|
|
1430
|
-
if (current.length > 0) {
|
|
1431
|
-
paragraphs.push(current.join(" "));
|
|
1432
|
-
current = [];
|
|
1433
|
-
}
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
current.push(trimmed);
|
|
1437
|
-
}
|
|
1438
|
-
if (current.length > 0) paragraphs.push(current.join(" "));
|
|
1439
|
-
return paragraphs;
|
|
1440
|
-
};
|
|
1441
|
-
const paragraphText = (lines) => compactParagraphs(lines).join(" ");
|
|
1442
1736
|
const stripManualNumbering = (value, line, fixes) => {
|
|
1443
1737
|
const stripped = value.trim().replace(MANUAL_NUMBERING_PREFIX_RE, "");
|
|
1444
1738
|
if (stripped !== value.trim()) fixes.push({
|
|
@@ -1455,8 +1749,32 @@ const isPageSize = (value) => value === "A4" || value === "Letter";
|
|
|
1455
1749
|
const isPageOrientation = (value) => value === "portrait" || value === "landscape";
|
|
1456
1750
|
//#endregion
|
|
1457
1751
|
//#region src/legal-source/validate.ts
|
|
1752
|
+
const WHOLE_TEXT_EMPHASIS = /^(?:\*\*[^*]+\*\*|__[^_]+__)$/u;
|
|
1753
|
+
/** Body strings of a block that a reader would expect to be prose, not a heading. */
|
|
1754
|
+
const bodyStrings = (block) => {
|
|
1755
|
+
switch (block.type) {
|
|
1756
|
+
case "paragraph":
|
|
1757
|
+
case "recital":
|
|
1758
|
+
case "clause":
|
|
1759
|
+
case "schedule": return block.paragraphs;
|
|
1760
|
+
case "list": return block.items;
|
|
1761
|
+
case "table": return block.table.rows.flat();
|
|
1762
|
+
case "title":
|
|
1763
|
+
case "signatures":
|
|
1764
|
+
case "pageBreak": return [];
|
|
1765
|
+
default: return [];
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1458
1768
|
const validateLegalDraft = (draft) => {
|
|
1459
1769
|
const diagnostics = [];
|
|
1770
|
+
for (const block of draft.blocks) if (bodyStrings(block).some((text) => WHOLE_TEXT_EMPHASIS.test(text.trim()))) {
|
|
1771
|
+
diagnostics.push({
|
|
1772
|
+
code: "whole-paragraph-emphasis",
|
|
1773
|
+
message: "A body paragraph, list item, or table cell is bold from end to end; use a clause heading for headings and keep bold for short labels.",
|
|
1774
|
+
severity: "warning"
|
|
1775
|
+
});
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1460
1778
|
if (!draft.meta.title?.trim()) diagnostics.push({
|
|
1461
1779
|
code: "missing-title",
|
|
1462
1780
|
message: "The draft must have a title.",
|
|
@@ -1616,35 +1934,15 @@ const paragraph = (text, styleId, runOptions = {}, numPr, pageBreakBefore = fals
|
|
|
1616
1934
|
...numPr ? { numPr } : {},
|
|
1617
1935
|
...pageBreakBefore ? { pageBreakBefore: true } : {}
|
|
1618
1936
|
},
|
|
1619
|
-
content:
|
|
1937
|
+
content: inlineMarkdownToRuns(text, {
|
|
1938
|
+
base: runOptions,
|
|
1939
|
+
placeholders: true
|
|
1940
|
+
})
|
|
1620
1941
|
});
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
let cursor = 0;
|
|
1626
|
-
for (const match of text.matchAll(PLACEHOLDER_PATTERN)) {
|
|
1627
|
-
const start = match.index;
|
|
1628
|
-
if (start > cursor) runs.push(textRun(text.slice(cursor, start), options));
|
|
1629
|
-
const inner = match.groups?.["inner"] ?? "";
|
|
1630
|
-
runs.push(textRun(inner, options, { highlight: "yellow" }));
|
|
1631
|
-
cursor = start + match[0].length;
|
|
1632
|
-
}
|
|
1633
|
-
if (cursor < text.length) runs.push(textRun(text.slice(cursor), options));
|
|
1634
|
-
return runs.length > 0 ? runs : [textRun(text, options)];
|
|
1635
|
-
};
|
|
1636
|
-
const textRun = (text, options = {}, extra = {}) => ({
|
|
1637
|
-
type: "run",
|
|
1638
|
-
formatting: {
|
|
1639
|
-
...options.bold ? { bold: true } : {},
|
|
1640
|
-
...options.italic ? { italic: true } : {},
|
|
1641
|
-
...extra.highlight ? { highlight: extra.highlight } : {}
|
|
1642
|
-
},
|
|
1643
|
-
content: [{
|
|
1644
|
-
type: "text",
|
|
1645
|
-
text,
|
|
1646
|
-
preserveSpace: true
|
|
1647
|
-
}]
|
|
1942
|
+
const plainParagraph = (text, styleId, runOptions = {}) => ({
|
|
1943
|
+
type: "paragraph",
|
|
1944
|
+
formatting: { styleId },
|
|
1945
|
+
content: plainTextRuns(text, runOptions)
|
|
1648
1946
|
});
|
|
1649
1947
|
const table = (headers, rows) => ({
|
|
1650
1948
|
type: "table",
|
|
@@ -1664,16 +1962,16 @@ const signatureTable = (parties) => {
|
|
|
1664
1962
|
signatory: "",
|
|
1665
1963
|
title: ""
|
|
1666
1964
|
}];
|
|
1667
|
-
const empty = () =>
|
|
1965
|
+
const empty = () => plainParagraph("", "SignatureSpacer");
|
|
1668
1966
|
const buildCell = (party) => {
|
|
1669
1967
|
const cellContent = [
|
|
1670
|
-
|
|
1968
|
+
plainParagraph(party.name, "SignatureParty", { bold: true }),
|
|
1671
1969
|
empty(),
|
|
1672
1970
|
empty(),
|
|
1673
|
-
|
|
1971
|
+
plainParagraph(SIGNATURE_LINE, "SignatureRule")
|
|
1674
1972
|
];
|
|
1675
|
-
if (party.signatory) cellContent.push(
|
|
1676
|
-
if (party.title) cellContent.push(
|
|
1973
|
+
if (party.signatory) cellContent.push(plainParagraph(party.signatory, "SignatureField"));
|
|
1974
|
+
if (party.title) cellContent.push(plainParagraph(party.title, "SignatureField", { italic: true }));
|
|
1677
1975
|
return {
|
|
1678
1976
|
type: "tableCell",
|
|
1679
1977
|
content: cellContent
|
|
@@ -2059,4 +2357,157 @@ const compileLegalSourceToDocx = async (source, options = {}) => {
|
|
|
2059
2357
|
};
|
|
2060
2358
|
};
|
|
2061
2359
|
//#endregion
|
|
2062
|
-
|
|
2360
|
+
//#region src/markdown/content.ts
|
|
2361
|
+
const para = (runs, styleId) => ({
|
|
2362
|
+
type: "paragraph",
|
|
2363
|
+
formatting: styleId ? { styleId } : {},
|
|
2364
|
+
content: runs.length > 0 ? runs : [textRun("")]
|
|
2365
|
+
});
|
|
2366
|
+
const listPara = (runs, rendering) => ({
|
|
2367
|
+
type: "paragraph",
|
|
2368
|
+
formatting: { numPr: {
|
|
2369
|
+
numId: rendering.numId,
|
|
2370
|
+
ilvl: rendering.level
|
|
2371
|
+
} },
|
|
2372
|
+
listRendering: rendering,
|
|
2373
|
+
content: runs.length > 0 ? runs : [textRun("")]
|
|
2374
|
+
});
|
|
2375
|
+
const cellOf = (cell) => ({
|
|
2376
|
+
type: "tableCell",
|
|
2377
|
+
content: [para(inlineTokensToRuns(cell.tokens, cell.text))]
|
|
2378
|
+
});
|
|
2379
|
+
const tableFromToken = (token) => ({
|
|
2380
|
+
type: "table",
|
|
2381
|
+
rows: [{
|
|
2382
|
+
type: "tableRow",
|
|
2383
|
+
cells: token.header.map((cell) => cellOf(cell))
|
|
2384
|
+
}, ...token.rows.map((row) => ({
|
|
2385
|
+
type: "tableRow",
|
|
2386
|
+
cells: row.map((cell) => cellOf(cell))
|
|
2387
|
+
}))]
|
|
2388
|
+
});
|
|
2389
|
+
const LIST_INDENT_STEP_TWIPS = 720;
|
|
2390
|
+
const buildListLevel = (ilvl, isBullet, start) => ({
|
|
2391
|
+
ilvl,
|
|
2392
|
+
...!isBullet && { start },
|
|
2393
|
+
numFmt: isBullet ? "bullet" : "decimal",
|
|
2394
|
+
lvlText: isBullet ? "•" : `%${ilvl + 1}.`,
|
|
2395
|
+
suffix: "tab",
|
|
2396
|
+
pPr: {
|
|
2397
|
+
indentLeft: LIST_INDENT_STEP_TWIPS * (ilvl + 1),
|
|
2398
|
+
indentFirstLine: -360,
|
|
2399
|
+
hangingIndent: true
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
/**
|
|
2403
|
+
* The numId a list renders under. The first list to reach a (numId, ilvl)
|
|
2404
|
+
* pair defines that level; a later list at the same depth under the same
|
|
2405
|
+
* parent shares it when it is the same kind (so sibling nested bullets share
|
|
2406
|
+
* one counter), and gets a numId of its own when it is not (a nested ordered
|
|
2407
|
+
* list must not inherit a sibling's bullet definition).
|
|
2408
|
+
*/
|
|
2409
|
+
const resolveListNumId = (numIds, parentNumId, level) => {
|
|
2410
|
+
const levels = numIds.levels.get(parentNumId);
|
|
2411
|
+
const existing = levels?.get(level.ilvl);
|
|
2412
|
+
if (levels !== void 0 && existing === void 0) {
|
|
2413
|
+
levels.set(level.ilvl, level);
|
|
2414
|
+
return parentNumId;
|
|
2415
|
+
}
|
|
2416
|
+
if (levels !== void 0 && existing !== void 0 && existing.numFmt === level.numFmt && existing.start === level.start) return parentNumId;
|
|
2417
|
+
const numId = numIds.next++;
|
|
2418
|
+
numIds.levels.set(numId, /* @__PURE__ */ new Map([[level.ilvl, level]]));
|
|
2419
|
+
return numId;
|
|
2420
|
+
};
|
|
2421
|
+
const listBlocks = (list, level, parentNumId, numIds) => {
|
|
2422
|
+
const out = [];
|
|
2423
|
+
const start = Number(list.start) || 1;
|
|
2424
|
+
const decimalLevels = Array.from({ length: level + 1 }, () => "decimal");
|
|
2425
|
+
const numId = resolveListNumId(numIds, parentNumId, buildListLevel(level, !list.ordered, start));
|
|
2426
|
+
for (const item of list.items) {
|
|
2427
|
+
const rendering = list.ordered ? {
|
|
2428
|
+
marker: `%${level + 1}.`,
|
|
2429
|
+
level,
|
|
2430
|
+
numId,
|
|
2431
|
+
isBullet: false,
|
|
2432
|
+
numFmt: "decimal",
|
|
2433
|
+
levelNumFmts: decimalLevels,
|
|
2434
|
+
...start !== 1 && { startOverride: start }
|
|
2435
|
+
} : {
|
|
2436
|
+
marker: "•",
|
|
2437
|
+
level,
|
|
2438
|
+
numId,
|
|
2439
|
+
isBullet: true
|
|
2440
|
+
};
|
|
2441
|
+
const inlineTokens = [];
|
|
2442
|
+
const nestedLists = [];
|
|
2443
|
+
for (const child of item.tokens) if (isTokenType(child, "list")) nestedLists.push(child);
|
|
2444
|
+
else inlineTokens.push(child);
|
|
2445
|
+
out.push(listPara(inlineTokensToRuns(inlineTokens, item.text), rendering));
|
|
2446
|
+
for (const nested of nestedLists) out.push(...listBlocks(nested, level + 1, numId, numIds));
|
|
2447
|
+
}
|
|
2448
|
+
return out;
|
|
2449
|
+
};
|
|
2450
|
+
const MAX_HEADING_LEVEL = 4;
|
|
2451
|
+
const blocksFromTokens = (tokens, numIds) => {
|
|
2452
|
+
const blocks = [];
|
|
2453
|
+
for (const token of tokens ?? []) if (isTokenType(token, "heading")) {
|
|
2454
|
+
const level = Math.min(Math.max(token.depth, 1), MAX_HEADING_LEVEL);
|
|
2455
|
+
blocks.push(para(inlineTokensToRuns(token.tokens, token.text), `Heading${level}`));
|
|
2456
|
+
} else if (isTokenType(token, "paragraph")) blocks.push(para(inlineTokensToRuns(token.tokens, token.text)));
|
|
2457
|
+
else if (isTokenType(token, "list")) {
|
|
2458
|
+
const numId = numIds.next++;
|
|
2459
|
+
numIds.levels.set(numId, /* @__PURE__ */ new Map());
|
|
2460
|
+
blocks.push(...listBlocks(token, 0, numId, numIds));
|
|
2461
|
+
} else if (isTokenType(token, "table")) blocks.push(tableFromToken(token));
|
|
2462
|
+
else if (isTokenType(token, "code")) for (const line of token.text.split("\n")) blocks.push(para([textRun(line.length > 0 ? line : " ", { mono: true })]));
|
|
2463
|
+
else if (isTokenType(token, "blockquote")) for (const inner of blocksFromTokens(token.tokens, numIds)) {
|
|
2464
|
+
const styled = inner.type === "paragraph" ? {
|
|
2465
|
+
...inner,
|
|
2466
|
+
formatting: {
|
|
2467
|
+
...inner.formatting,
|
|
2468
|
+
styleId: "Quote"
|
|
2469
|
+
}
|
|
2470
|
+
} : inner;
|
|
2471
|
+
blocks.push(styled);
|
|
2472
|
+
}
|
|
2473
|
+
else if (token.type === "hr") blocks.push(para([textRun("———")]));
|
|
2474
|
+
else if (token.type !== "space" && "text" in token && typeof token.text === "string" && token.text.trim().length > 0) blocks.push(para([textRun(token.text)]));
|
|
2475
|
+
return blocks;
|
|
2476
|
+
};
|
|
2477
|
+
const buildNumbering = (numIdLevels) => {
|
|
2478
|
+
const abstractNums = [];
|
|
2479
|
+
const nums = [];
|
|
2480
|
+
for (const [numId, levels] of numIdLevels) {
|
|
2481
|
+
const sortedLevels = [...levels.entries()].sort(([a], [b]) => a - b).map(([, lvl]) => lvl);
|
|
2482
|
+
abstractNums.push({
|
|
2483
|
+
abstractNumId: numId,
|
|
2484
|
+
multiLevelType: sortedLevels.length > 1 ? "multilevel" : "singleLevel",
|
|
2485
|
+
levels: sortedLevels
|
|
2486
|
+
});
|
|
2487
|
+
nums.push({
|
|
2488
|
+
numId,
|
|
2489
|
+
abstractNumId: numId
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
return {
|
|
2493
|
+
abstractNums,
|
|
2494
|
+
nums
|
|
2495
|
+
};
|
|
2496
|
+
};
|
|
2497
|
+
/**
|
|
2498
|
+
* Parse GFM markdown into document blocks plus the numbering its lists need.
|
|
2499
|
+
* Synchronous. The caller places the blocks into a `Document` of its own
|
|
2500
|
+
* (page geometry, styles, and presets are the host's decision).
|
|
2501
|
+
*/
|
|
2502
|
+
const compileMarkdownToContent = (markdown) => {
|
|
2503
|
+
const numIds = {
|
|
2504
|
+
next: 1,
|
|
2505
|
+
levels: /* @__PURE__ */ new Map()
|
|
2506
|
+
};
|
|
2507
|
+
return {
|
|
2508
|
+
content: blocksFromTokens(lexMarkdown(markdown), numIds),
|
|
2509
|
+
...numIds.levels.size > 0 && { numbering: buildNumbering(numIds.levels) }
|
|
2510
|
+
};
|
|
2511
|
+
};
|
|
2512
|
+
//#endregion
|
|
2513
|
+
export { DOCX_CONFORMANCE_CLASSES, DOCX_PACKAGE_ISSUE_CODES, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, compileMarkdownToContent, parseLegalSource, sanitizeExternalUrl, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
|
package/dist/model/document.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as ImagePosition, $t as TextBox, A as ComplexField, An as TableFormatting, At as SdtType, B as FieldCharContent, Bn as KnownBorderStyle, Bt as ShapeTextBody, C as BookmarkStart, Cn as SpacingExplicit, Ct as PictureWatermark, D as CommentRangeEnd, Dn as TableBorders, Dt as RunContent, E as Comment, En as TabStopAlignment, Et as Run, F as DrawingRawXmlMode, Fn as TextEffect, Ft as Shape, G as FootnoteProperties, Gt as TabContent, H as FooterReference, Hn as ThemeColorSlot, Ht as SimpleField, I as Endnote, In as TextFormatting, It as ShapeContent, J as HeaderReference, Jt as TableCellPropertyChange, K as HeaderFooter, Kt as Table, L as EndnotePosition, Ln as UnderlineStyle, Lt as ShapeFill, M as Deletion, Mn as TableMeasurement, Mt as SectionProperties, N as DocumentBody, Nn as TableRowFormatting, Nt as SectionPropertyChange, O as CommentRangeStart, On as TableCellBorders, Ot as RunPropertyChange, P as DrawingContent, Pn as TableWidthType, Pt as SectionStart, Q as ImagePadding, Qt as TableStructuralChangeInfo, R as EndnoteProperties, Rn as BorderSpec, Rt as ShapeGeometryAdjustment, S as BookmarkEnd, Sn as ParagraphFormatting, St as ParagraphPropertyChange, T as Column, Tn as TabStop, Tt as PropertyChangeInfo, U as Footnote, Ut as SoftHyphenContent, V as FieldType, Vn as ShadingProperties, Vt as ShapeType, W as FootnotePosition, Wt as SymbolContent, X as Image, Xt as TableRow, Y as Hyperlink, Yt as TablePropertyChange, Z as ImageCrop, Zt as TableRowPropertyChange, _ as ThemeColorScheme, _n as ConditionalFormatStyle, _t as NoteReferenceContent, a as DocxPackage, an as Watermark, at as InstrTextContent, b as BlockContent, bn as LineSpacingRule, bt as ParagraphContent, c as FontTable, cn as AbstractNumbering, ct as MathEquation, d as RelationshipMap, dn as ListMarkerFormatting, dt as MoveFromRangeStart, en as TextContent, et as ImageSize, f as RelationshipType, fn as ListRendering, ft as MoveTo, g as Theme, gn as CellMargins, gt as NoteNumberRestart, h as StyleType, hn as NumberingInstance, ht as NoBreakHyphenContent, i as DocxConformanceClass, in as VerticalAlign, it as Insertion, j as DRAWING_RAW_XML_MODES, jn as TableLook, jt as Section, k as CommentReference, kn as TableCellFormatting, kt as SdtProperties, l as MediaFile, ln as LevelSuffix, lt as MoveFrom, m as StyleDefinitions, mn as NumberingDefinitions, mt as MoveToRangeStart, n as Document, nn as TrackedChangeInfo, nt as ImageWrap, o as DocDefaults, on as isOoxmlSymbolCharacter, ot as LineNumberRestart, p as Style, pn as NumberFormat, pt as MoveToRangeEnd, q as HeaderFooterType, qt as TableCell, r as DocumentSettings, rn as TrackedRunChange, rt as InlineSdt, s as FontInfo, sn as normalizeRevisionId, st as MAX_REVISION_ID, t as DOCX_CONFORMANCE_CLASSES, tn as TextWatermark, tt as ImageTransform, u as Relationship, un as ListLevel, ut as MoveFromRangeEnd, v as ThemeFont, vn as EmphasisMark, vt as PageOrientation, w as BreakContent, wn as TabLeader, wt as PositionalTab, x as BlockSdt, xn as ParagraphAlignment, xt as ParagraphMarkChange, y as ThemeFontScheme, yn as FloatingTableProperties, yt as Paragraph, z as Field, zn as ColorValue, zt as ShapeOutline } from "../document-
|
|
1
|
+
import { $ as ImagePosition, $t as TextBox, A as ComplexField, An as TableFormatting, At as SdtType, B as FieldCharContent, Bn as KnownBorderStyle, Bt as ShapeTextBody, C as BookmarkStart, Cn as SpacingExplicit, Ct as PictureWatermark, D as CommentRangeEnd, Dn as TableBorders, Dt as RunContent, E as Comment, En as TabStopAlignment, Et as Run, F as DrawingRawXmlMode, Fn as TextEffect, Ft as Shape, G as FootnoteProperties, Gt as TabContent, H as FooterReference, Hn as ThemeColorSlot, Ht as SimpleField, I as Endnote, In as TextFormatting, It as ShapeContent, J as HeaderReference, Jt as TableCellPropertyChange, K as HeaderFooter, Kt as Table, L as EndnotePosition, Ln as UnderlineStyle, Lt as ShapeFill, M as Deletion, Mn as TableMeasurement, Mt as SectionProperties, N as DocumentBody, Nn as TableRowFormatting, Nt as SectionPropertyChange, O as CommentRangeStart, On as TableCellBorders, Ot as RunPropertyChange, P as DrawingContent, Pn as TableWidthType, Pt as SectionStart, Q as ImagePadding, Qt as TableStructuralChangeInfo, R as EndnoteProperties, Rn as BorderSpec, Rt as ShapeGeometryAdjustment, S as BookmarkEnd, Sn as ParagraphFormatting, St as ParagraphPropertyChange, T as Column, Tn as TabStop, Tt as PropertyChangeInfo, U as Footnote, Ut as SoftHyphenContent, V as FieldType, Vn as ShadingProperties, Vt as ShapeType, W as FootnotePosition, Wt as SymbolContent, X as Image, Xt as TableRow, Y as Hyperlink, Yt as TablePropertyChange, Z as ImageCrop, Zt as TableRowPropertyChange, _ as ThemeColorScheme, _n as ConditionalFormatStyle, _t as NoteReferenceContent, a as DocxPackage, an as Watermark, at as InstrTextContent, b as BlockContent, bn as LineSpacingRule, bt as ParagraphContent, c as FontTable, cn as AbstractNumbering, ct as MathEquation, d as RelationshipMap, dn as ListMarkerFormatting, dt as MoveFromRangeStart, en as TextContent, et as ImageSize, f as RelationshipType, fn as ListRendering, ft as MoveTo, g as Theme, gn as CellMargins, gt as NoteNumberRestart, h as StyleType, hn as NumberingInstance, ht as NoBreakHyphenContent, i as DocxConformanceClass, in as VerticalAlign, it as Insertion, j as DRAWING_RAW_XML_MODES, jn as TableLook, jt as Section, k as CommentReference, kn as TableCellFormatting, kt as SdtProperties, l as MediaFile, ln as LevelSuffix, lt as MoveFrom, m as StyleDefinitions, mn as NumberingDefinitions, mt as MoveToRangeStart, n as Document, nn as TrackedChangeInfo, nt as ImageWrap, o as DocDefaults, on as isOoxmlSymbolCharacter, ot as LineNumberRestart, p as Style, pn as NumberFormat, pt as MoveToRangeEnd, q as HeaderFooterType, qt as TableCell, r as DocumentSettings, rn as TrackedRunChange, rt as InlineSdt, s as FontInfo, sn as normalizeRevisionId, st as MAX_REVISION_ID, t as DOCX_CONFORMANCE_CLASSES, tn as TextWatermark, tt as ImageTransform, u as Relationship, un as ListLevel, ut as MoveFromRangeEnd, v as ThemeFont, vn as EmphasisMark, vt as PageOrientation, w as BreakContent, wn as TabLeader, wt as PositionalTab, x as BlockSdt, xn as ParagraphAlignment, xt as ParagraphMarkChange, y as ThemeFontScheme, yn as FloatingTableProperties, yt as Paragraph, z as Field, zn as ColorValue, zt as ShapeOutline } from "../document-C4ms2O35.js";
|
|
2
2
|
export { type AbstractNumbering, type BlockContent, type BlockSdt, type BookmarkEnd, type BookmarkStart, type BorderSpec, type BreakContent, type CellMargins, type ColorValue, type Column, type Comment, type CommentRangeEnd, type CommentRangeStart, type CommentReference, type ComplexField, type ConditionalFormatStyle, DOCX_CONFORMANCE_CLASSES, DRAWING_RAW_XML_MODES, type Deletion, type DocDefaults, Document, type DocumentBody, DocumentSettings, DocxConformanceClass, DocxPackage, type DrawingContent, type DrawingRawXmlMode, type EmphasisMark, type Endnote, type EndnotePosition, type EndnoteProperties, type Field, type FieldCharContent, type FieldType, type FloatingTableProperties, type FontInfo, type FontTable, type FooterReference, type Footnote, type FootnotePosition, type FootnoteProperties, type HeaderFooter, type HeaderFooterType, type HeaderReference, type Hyperlink, type Image, type ImageCrop, type ImagePadding, type ImagePosition, type ImageSize, type ImageTransform, type ImageWrap, type InlineSdt, type Insertion, type InstrTextContent, type KnownBorderStyle, type LevelSuffix, type LineNumberRestart, type LineSpacingRule, type ListLevel, type ListMarkerFormatting, type ListRendering, MAX_REVISION_ID, type MathEquation, type MediaFile, type MoveFrom, type MoveFromRangeEnd, type MoveFromRangeStart, type MoveTo, type MoveToRangeEnd, type MoveToRangeStart, type NoBreakHyphenContent, type NoteNumberRestart, type NoteReferenceContent, type NumberFormat, type NumberingDefinitions, type NumberingInstance, type PageOrientation, type Paragraph, type ParagraphAlignment, type ParagraphContent, type ParagraphFormatting, type ParagraphMarkChange, type ParagraphPropertyChange, type PictureWatermark, type PositionalTab, type PropertyChangeInfo, type Relationship, type RelationshipMap, type RelationshipType, type Run, type RunContent, type RunPropertyChange, type SdtProperties, type SdtType, type Section, type SectionProperties, type SectionPropertyChange, type SectionStart, type ShadingProperties, type Shape, type ShapeContent, type ShapeFill, type ShapeGeometryAdjustment, type ShapeOutline, type ShapeTextBody, type ShapeType, type SimpleField, type SoftHyphenContent, type SpacingExplicit, type Style, type StyleDefinitions, type StyleType, type SymbolContent, type TabContent, type TabLeader, type TabStop, type TabStopAlignment, type Table, type TableBorders, type TableCell, type TableCellBorders, type TableCellFormatting, type TableCellPropertyChange, type TableFormatting, type TableLook, type TableMeasurement, type TablePropertyChange, type TableRow, type TableRowFormatting, type TableRowPropertyChange, type TableStructuralChangeInfo, type TableWidthType, type TextBox, type TextContent, type TextEffect, type TextFormatting, type TextWatermark, type Theme, type ThemeColorScheme, type ThemeColorSlot, type ThemeFont, type ThemeFontScheme, type TrackedChangeInfo, type TrackedRunChange, type UnderlineStyle, type VerticalAlign, type Watermark, isOoxmlSymbolCharacter, normalizeRevisionId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/docx-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Typed OOXML/DOCX model, validation, serialization, legal-source compilation, and browser-native package projection.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"better-result": "3.0.1",
|
|
64
64
|
"fast-xml-parser": "^5.10.1",
|
|
65
|
-
"jszip": "3.10.1"
|
|
65
|
+
"jszip": "3.10.1",
|
|
66
|
+
"marked": "^18.0.5"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"bun-types": "1.4.0",
|