smartrte-core 1.0.0-beta.4 → 1.0.0-beta.6
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/CHANGELOG.md +11 -0
- package/dist/foundation/atom/declarations.js +4 -0
- package/dist/foundation/atom/formats.d.ts +33 -2
- package/dist/foundation/atom/formats.js +50 -2
- package/dist/foundation/atom/schema.js +9 -0
- package/dist/foundation/atom/types.d.ts +1 -1
- package/dist/foundation/block/formats.d.ts +2 -0
- package/dist/foundation/block/formats.js +2 -0
- package/dist/foundation/block/schema.js +13 -1
- package/dist/foundation/clipboard/normalizers.js +18 -2
- package/dist/foundation/formats/docx/export.js +25 -0
- package/dist/foundation/formats/featureCodecs.d.ts +8 -7
- package/dist/foundation/formats/featureCodecs.js +10 -9
- package/dist/foundation/formats/fidelity.d.ts +1 -1
- package/dist/foundation/formats/fidelity.js +18 -0
- package/dist/foundation/formats/pdf/format.js +24 -1
- package/dist/foundation/list/formats.js +44 -3
- package/dist/foundation/surface/renderer.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.6
|
|
4
|
+
|
|
5
|
+
- Fix pasting a long, list-free document (many short paragraphs, no images) being slow enough to look broken — the Office/Google Docs list normalizers recursed into every paragraph hunting for nested lists a `<p>` can never structurally contain. Pasting a few thousand blocks now completes in well under a second instead of multiple seconds to tens of seconds. See `docs/bugs/clipboard-paste-slow-on-large-list-free-documents.md` for the full investigation, including a known remaining limitation for very large (10,000+ block) pastes.
|
|
6
|
+
|
|
7
|
+
## 1.0.0-beta.5
|
|
8
|
+
|
|
9
|
+
- Add a per-block `lineHeight` attribute (paragraph, heading, blockquote, code block) — a unitless multiplier matching CSS `line-height`'s own unitless mode (`1`, `1.5`, `2`, ...), consistent with how `align`/`indentLevel` are represented. Absence means no override (the browser/font's own natural line-height), not a forced `1`. Full HTML round-trip fidelity via a dedicated `data-smart-line-height` marker — deliberately does **not** fall back to reading a bare CSS `line-height` value on import (unlike `align`/`indentLevel`'s own CSS fallbacks): real captured clipboard fixtures show `line-height` is frequently an ambient, whole-document default baked onto every paragraph, not a deliberate per-paragraph choice, and recognizing it would have silently misattributed that default as an explicit override on every pasted document. Real Word line spacing (`w:spacing`/`w:line`/`w:lineRule="auto"`) on DOCX export; declared `lossy` there (confirmed empirically that DOCX reimport via mammoth drops it) and `unsupported` (content preserved) in Markdown. Inherits `full` PDF fidelity for free, since this package's "Save as PDF" prints the same HTML export.
|
|
10
|
+
- Add a `page_break` atom node — a print/export pagination marker, distinct from the existing `divider` (horizontal rule) node. Exports to a real paginating marker in HTML (and this package's own "Save as PDF", which prints that same HTML) and a real native Word page break in DOCX; declared `unsupported` in Markdown (no pagination concept there), preserved as an inert comment rather than silently dropped. Add an `AtomDeclaration` for `divider` so it can be inserted via the standard atom-insertion command, not just recovered from pasted `<hr>`. Fix `divider` silently exporting as an empty paragraph in DOCX (no visible line at all).
|
|
11
|
+
- Fix a real HTML round-trip bug found while building the above: a div-tagged atom's own round-trip marker (`block_formula`, and the new `page_break`) was silently deleted on reimport — treated as a meaningless transparent wrapper div and recursed into (destroying it) before ever reaching the atom-parsing logic. See `docs/bugs/html-import-div-atom-markers-treated-as-transparent-wrapper.md`.
|
|
12
|
+
- Fix formulas rendering as invisible empty placeholders in "Save as PDF" output — the print document never baked real KaTeX HTML (the same gap already fixed once for a different static-HTML consumer) and never loaded KaTeX's own CSS at all (a brand-new, isolated print window shares none of the host page's stylesheets). See `docs/bugs/pdf-export-formula-invisible-and-misleading-label.md`.
|
|
13
|
+
|
|
3
14
|
## 1.0.0-beta.4
|
|
4
15
|
|
|
5
16
|
- Fix HTML import (paste and loading a document's initial value) rendering real content as unreadable `[Unsupported: ...]` placeholders when a bare `<span>`/`<b>`/other inline-formatting element sits directly at the document root, or is interleaved between real blocks (a `<blockquote>`, `<div>`-wrapped lines, images) with no wrapping `<p>` — a common shape from legacy, pre-migration editor exports. Also fixes the same gap in `<blockquote>`/table cells/list items for content interleaved between blocks (previously only content entirely before any block content was recovered correctly).
|
|
@@ -14,4 +14,8 @@ export const atomDeclarations = [
|
|
|
14
14
|
{ type: "block_formula", kind: "formula", group: "block", validate: formulaValid },
|
|
15
15
|
{ type: "video", kind: "video", group: "block", validate: mediaValid("video") },
|
|
16
16
|
{ type: "audio", kind: "audio", group: "block", validate: mediaValid("audio") },
|
|
17
|
+
// No attrs to validate - a divider/page break carries none (see
|
|
18
|
+
// atom/schema.ts's own comments on both node types).
|
|
19
|
+
{ type: "divider", kind: "divider", group: "block", validate: () => true },
|
|
20
|
+
{ type: "page_break", kind: "pageBreak", group: "block", validate: () => true },
|
|
17
21
|
];
|
|
@@ -4,9 +4,20 @@ export declare const atomToHtml: (node: SmartElementNode, options?: {
|
|
|
4
4
|
renderFormulaHtml?: boolean;
|
|
5
5
|
}) => string;
|
|
6
6
|
export declare const atomFromHtmlElement: (rawElement: Element) => SmartElementNode | null;
|
|
7
|
+
/**
|
|
8
|
+
* page_break: Markdown has no pagination concept at all (declared
|
|
9
|
+
* `unsupported` in formats/fidelity.ts) - emitting nothing would silently
|
|
10
|
+
* delete the marker with no trace, the same class of bug this project has
|
|
11
|
+
* hit before for images/formulas (see formats/fidelity.ts's own
|
|
12
|
+
* images-media/formulas notes on that history). An HTML comment is inert
|
|
13
|
+
* in every real Markdown renderer (so it never appears as visible garbage
|
|
14
|
+
* text) but keeps the marker's *position* recorded in the exported file -
|
|
15
|
+
* genuinely honest `unsupported`, not a round-trippable format: nothing
|
|
16
|
+
* parses this comment back into a page_break node on import.
|
|
17
|
+
*/
|
|
7
18
|
export declare const atomToMarkdown: (node: SmartElementNode) => string;
|
|
8
19
|
export interface AtomDocxRun {
|
|
9
|
-
readonly kind: "image" | "text";
|
|
20
|
+
readonly kind: "image" | "text" | "pageBreak";
|
|
10
21
|
readonly source: string;
|
|
11
22
|
readonly alt?: string;
|
|
12
23
|
}
|
|
@@ -16,9 +27,29 @@ export interface AtomDocxRun {
|
|
|
16
27
|
* <m:oMath> zone, not translated to real OMML and not rendered as an
|
|
17
28
|
* image. Word will show the raw LaTeX string, not typeset math - this is
|
|
18
29
|
* `kind: "text"`, not `"image"`, to describe that honestly.
|
|
30
|
+
*
|
|
31
|
+
* page_break returns `kind: "pageBreak"` (`source` unused, kept empty) -
|
|
32
|
+
* the real docx/export.ts blockXml dispatcher turns this into a genuine
|
|
33
|
+
* `<w:br w:type="page"/>` run, matching what Word's own Ctrl+Enter inserts,
|
|
34
|
+
* not a visual-only text marker.
|
|
19
35
|
*/
|
|
20
36
|
export declare const atomToDocx: (node: SmartElementNode) => AtomDocxRun;
|
|
37
|
+
/**
|
|
38
|
+
* IMPORTANT: this function is NOT what actually produces this package's
|
|
39
|
+
* real "Save as PDF" output today. That feature (react/src/adapters/
|
|
40
|
+
* pdfPrint.ts's printSmartDocumentAsPdf) opens the browser's native print
|
|
41
|
+
* dialog against `buildPdfPrintDocument` (formats/pdf/format.ts), which
|
|
42
|
+
* calls `serializeCanonicalListHtml` - i.e. `atomToHtml` above, not this
|
|
43
|
+
* function. This function only backs the per-node PDF `FeatureFormatCodec`
|
|
44
|
+
* fidelity-declaration cell (featureCodecs.ts) and its own unit tests - a
|
|
45
|
+
* real pagination pipeline built around per-node PDF primitives (a PDF
|
|
46
|
+
* library call, not a browser print) would consume this `"pageBreak"` kind
|
|
47
|
+
* to start a new page, but no such pipeline exists in this codebase today.
|
|
48
|
+
* Real page-break pagination in the actual "Save as PDF" feature comes
|
|
49
|
+
* entirely from atomToHtml's `break-before: page` marker, verified via
|
|
50
|
+
* e2e against the real print document, not via this function.
|
|
51
|
+
*/
|
|
21
52
|
export declare const atomToPdf: (node: SmartElementNode) => {
|
|
22
|
-
kind: "image" | "text";
|
|
53
|
+
kind: "image" | "text" | "pageBreak";
|
|
23
54
|
value: string;
|
|
24
55
|
};
|
|
@@ -63,6 +63,16 @@ export const atomToHtml = (node, options) => {
|
|
|
63
63
|
}
|
|
64
64
|
if (node.type === "divider")
|
|
65
65
|
return `<hr data-smart-id="${escape(node.id)}" data-smart-type="divider">`;
|
|
66
|
+
// Both `break-before: page` (the current CSS Fragmentation property) and
|
|
67
|
+
// `page-break-before: always` (its older, still widely-honored alias) are
|
|
68
|
+
// set directly on the marker so it actually paginates wherever this HTML
|
|
69
|
+
// is printed - including this package's own "Save as PDF" (a real browser
|
|
70
|
+
// print of this exact HTML; see react/src/adapters/pdfPrint.ts's own doc
|
|
71
|
+
// comment). A screen-media renderer (the live editor) ignores both
|
|
72
|
+
// properties entirely, so this is safe to always emit rather than gating
|
|
73
|
+
// behind @media print.
|
|
74
|
+
if (node.type === "page_break")
|
|
75
|
+
return `<div data-smart-id="${escape(node.id)}" data-smart-type="page_break" style="break-before: page; page-break-before: always;" role="separator" aria-label="Page break"></div>`;
|
|
66
76
|
throw new Error(`Unsupported atom type "${node.type}".`);
|
|
67
77
|
};
|
|
68
78
|
export const atomFromHtmlElement = (rawElement) => {
|
|
@@ -108,8 +118,21 @@ export const atomFromHtmlElement = (rawElement) => {
|
|
|
108
118
|
}
|
|
109
119
|
if (type === "divider" || element.tagName === "HR")
|
|
110
120
|
return { type: "divider", id };
|
|
121
|
+
if (type === "page_break")
|
|
122
|
+
return { type: "page_break", id };
|
|
111
123
|
return null;
|
|
112
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* page_break: Markdown has no pagination concept at all (declared
|
|
127
|
+
* `unsupported` in formats/fidelity.ts) - emitting nothing would silently
|
|
128
|
+
* delete the marker with no trace, the same class of bug this project has
|
|
129
|
+
* hit before for images/formulas (see formats/fidelity.ts's own
|
|
130
|
+
* images-media/formulas notes on that history). An HTML comment is inert
|
|
131
|
+
* in every real Markdown renderer (so it never appears as visible garbage
|
|
132
|
+
* text) but keeps the marker's *position* recorded in the exported file -
|
|
133
|
+
* genuinely honest `unsupported`, not a round-trippable format: nothing
|
|
134
|
+
* parses this comment back into a page_break node on import.
|
|
135
|
+
*/
|
|
113
136
|
export const atomToMarkdown = (node) => {
|
|
114
137
|
if (node.type === "image" || node.type === "block_image")
|
|
115
138
|
return `})`;
|
|
@@ -120,6 +143,8 @@ export const atomToMarkdown = (node) => {
|
|
|
120
143
|
return `[${node.type}: ${String(node.attrs?.src || "")}](${String(node.attrs?.src || "")})`;
|
|
121
144
|
if (node.type === "divider")
|
|
122
145
|
return "---";
|
|
146
|
+
if (node.type === "page_break")
|
|
147
|
+
return "<!-- page break -->";
|
|
123
148
|
return "";
|
|
124
149
|
};
|
|
125
150
|
/**
|
|
@@ -128,6 +153,11 @@ export const atomToMarkdown = (node) => {
|
|
|
128
153
|
* <m:oMath> zone, not translated to real OMML and not rendered as an
|
|
129
154
|
* image. Word will show the raw LaTeX string, not typeset math - this is
|
|
130
155
|
* `kind: "text"`, not `"image"`, to describe that honestly.
|
|
156
|
+
*
|
|
157
|
+
* page_break returns `kind: "pageBreak"` (`source` unused, kept empty) -
|
|
158
|
+
* the real docx/export.ts blockXml dispatcher turns this into a genuine
|
|
159
|
+
* `<w:br w:type="page"/>` run, matching what Word's own Ctrl+Enter inserts,
|
|
160
|
+
* not a visual-only text marker.
|
|
131
161
|
*/
|
|
132
162
|
export const atomToDocx = (node) => node.type === "formula" || node.type === "block_formula"
|
|
133
163
|
? { kind: "text", source: String(node.attrs?.source || "") }
|
|
@@ -135,9 +165,27 @@ export const atomToDocx = (node) => node.type === "formula" || node.type === "bl
|
|
|
135
165
|
? { kind: "image", source: String(node.attrs?.src || ""), alt: String(node.attrs?.alt || "") }
|
|
136
166
|
: node.type === "divider"
|
|
137
167
|
? { kind: "text", source: "---" }
|
|
138
|
-
:
|
|
168
|
+
: node.type === "page_break"
|
|
169
|
+
? { kind: "pageBreak", source: "" }
|
|
170
|
+
: { kind: "text", source: `[${node.type}: ${String(node.attrs?.src || "")}]` };
|
|
171
|
+
/**
|
|
172
|
+
* IMPORTANT: this function is NOT what actually produces this package's
|
|
173
|
+
* real "Save as PDF" output today. That feature (react/src/adapters/
|
|
174
|
+
* pdfPrint.ts's printSmartDocumentAsPdf) opens the browser's native print
|
|
175
|
+
* dialog against `buildPdfPrintDocument` (formats/pdf/format.ts), which
|
|
176
|
+
* calls `serializeCanonicalListHtml` - i.e. `atomToHtml` above, not this
|
|
177
|
+
* function. This function only backs the per-node PDF `FeatureFormatCodec`
|
|
178
|
+
* fidelity-declaration cell (featureCodecs.ts) and its own unit tests - a
|
|
179
|
+
* real pagination pipeline built around per-node PDF primitives (a PDF
|
|
180
|
+
* library call, not a browser print) would consume this `"pageBreak"` kind
|
|
181
|
+
* to start a new page, but no such pipeline exists in this codebase today.
|
|
182
|
+
* Real page-break pagination in the actual "Save as PDF" feature comes
|
|
183
|
+
* entirely from atomToHtml's `break-before: page` marker, verified via
|
|
184
|
+
* e2e against the real print document, not via this function.
|
|
185
|
+
*/
|
|
139
186
|
export const atomToPdf = (node) => node.type === "formula" || node.type === "block_formula" ? { kind: "text", value: String(node.attrs?.source || "") }
|
|
140
187
|
: node.type === "video" ? { kind: "image", value: String(node.attrs?.poster || node.attrs?.src || "") }
|
|
141
188
|
: node.type === "image" || node.type === "block_image" ? { kind: "image", value: String(node.attrs?.src || "") }
|
|
142
189
|
: node.type === "divider" ? { kind: "text", value: "---" }
|
|
143
|
-
: { kind: "
|
|
190
|
+
: node.type === "page_break" ? { kind: "pageBreak", value: "" }
|
|
191
|
+
: { kind: "text", value: String(node.attrs?.src || "") };
|
|
@@ -55,4 +55,13 @@ export const atomNodeSpecs = [
|
|
|
55
55
|
// case for "hr"), so every pasted <hr> fell to the generic
|
|
56
56
|
// "unrecognized tag" fallback and rendered as "[Unsupported: hr]".
|
|
57
57
|
{ type: "divider", group: "block", atomic: true, selectable: true, marks: "" },
|
|
58
|
+
// A print/export pagination marker - deliberately its own node type, not
|
|
59
|
+
// a `divider` variant: a horizontal line is decorative content with no
|
|
60
|
+
// export-time meaning beyond "draw a line," while a page break's entire
|
|
61
|
+
// purpose is what happens at export/print time (real PDF pagination, a
|
|
62
|
+
// DOCX page-break run, `break-before: page` in printed HTML) - see
|
|
63
|
+
// atom/formats.ts's atomToPdf/atomToDocx and formats/docx/export.ts for
|
|
64
|
+
// where that distinction actually matters. No content, no attrs, same as
|
|
65
|
+
// divider.
|
|
66
|
+
{ type: "page_break", group: "block", atomic: true, selectable: true, marks: "" },
|
|
58
67
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Attrs, SmartDocument, SmartOperation, SmartSchema } from "../types.js";
|
|
2
2
|
import type { PositionLookup, ResolvedScope } from "../scope/types.js";
|
|
3
|
-
export type AtomKind = "image" | "formula" | "video" | "audio";
|
|
3
|
+
export type AtomKind = "image" | "formula" | "video" | "audio" | "divider" | "pageBreak";
|
|
4
4
|
export type AtomStatus = "pending" | "ready" | "error";
|
|
5
5
|
export interface AtomDeclaration {
|
|
6
6
|
readonly type: string;
|
|
@@ -10,6 +10,8 @@ export interface CanonicalDocxBlock {
|
|
|
10
10
|
readonly alignment?: "left" | "center" | "right" | "justify";
|
|
11
11
|
/** DOCX left indentation in twips (720 twips per canonical indent level). */
|
|
12
12
|
readonly indentTwips?: number;
|
|
13
|
+
/** Word's native "multiple" line spacing unit: 240ths of a single line (240 = 1.0, 360 = 1.5). See formats/docx/export.ts's lineHeightOf for the same convention in the real exporter. */
|
|
14
|
+
readonly lineSpacing240ths?: number;
|
|
13
15
|
readonly language?: string;
|
|
14
16
|
readonly quoteDepth?: number;
|
|
15
17
|
}
|
|
@@ -20,6 +20,7 @@ export const blockToDocxEntry = (node, quoteDepth = 0) => {
|
|
|
20
20
|
const alignment = typeof node.attrs?.align === "string" && ["left", "center", "right", "justify"].includes(node.attrs.align)
|
|
21
21
|
? node.attrs.align : undefined;
|
|
22
22
|
const indentLevel = Math.max(0, Number(node.attrs?.indentLevel) || 0);
|
|
23
|
+
const lineHeight = Number(node.attrs?.lineHeight) || 0;
|
|
23
24
|
return {
|
|
24
25
|
nodeId: node.id,
|
|
25
26
|
kind: node.type === "code_block" ? "code" : node.type === "heading" ? "heading" : "paragraph",
|
|
@@ -28,6 +29,7 @@ export const blockToDocxEntry = (node, quoteDepth = 0) => {
|
|
|
28
29
|
...(level ? { outlineLevel: level - 1 } : {}),
|
|
29
30
|
...(alignment ? { alignment } : {}),
|
|
30
31
|
...(indentLevel ? { indentTwips: indentLevel * 720 } : {}),
|
|
32
|
+
...(lineHeight ? { lineSpacing240ths: Math.round(lineHeight * 240) } : {}),
|
|
31
33
|
...(node.type === "code_block" && typeof node.attrs?.language === "string" ? { language: node.attrs.language } : {}),
|
|
32
34
|
...(quoteDepth ? { quoteDepth } : {}),
|
|
33
35
|
};
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
const stringAttr = { validate: (value) => typeof value === "string" };
|
|
2
2
|
const alignmentAttr = { validate: (value) => ["left", "center", "right", "justify"].includes(String(value)) };
|
|
3
3
|
const indentLevelAttr = { validate: (value) => Number.isInteger(value) && Number(value) >= 0 && Number(value) <= 10 };
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A unitless multiplier, matching CSS `line-height`'s own unitless mode
|
|
6
|
+
* (e.g. `1.5` means "1.5x the element's own font size") - deliberately not
|
|
7
|
+
* a fixed px/pt value, which would stop scaling correctly the moment
|
|
8
|
+
* `fontSize` changes on the same block. Range is generous but bounded
|
|
9
|
+
* (Google Docs' own preset list tops out at 2.5; 0.1-10 comfortably covers
|
|
10
|
+
* every preset plus realistic custom values without accepting nonsense
|
|
11
|
+
* like a negative multiplier). Absence of this attribute (not "1") means
|
|
12
|
+
* "no override" - the block renders at the browser/font's own natural
|
|
13
|
+
* line-height, exactly like `align`/`indentLevel` already work.
|
|
14
|
+
*/
|
|
15
|
+
const lineHeightAttr = { validate: (value) => typeof value === "number" && Number.isFinite(value) && value >= 0.1 && value <= 10 };
|
|
16
|
+
const blockAttrs = { align: alignmentAttr, indentLevel: indentLevelAttr, lineHeight: lineHeightAttr };
|
|
5
17
|
/** Node specs owned by the block family - extracted out of foundation/schema.ts's previously hardcoded foundationSchema literal (Phase 10). */
|
|
6
18
|
export const blockNodeSpecs = [
|
|
7
19
|
{ type: "paragraph", group: "block", content: "inline*", attributes: blockAttrs },
|
|
@@ -2,6 +2,20 @@ import { parseCanonicalListMarkdown, serializeCanonicalListHtml } from "../list/
|
|
|
2
2
|
const listTags = new Set(["UL", "OL"]);
|
|
3
3
|
const blockSelector = ":scope > p, :scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6, :scope > ul, :scope > ol, :scope > blockquote, :scope > pre, :scope > table, :scope > div";
|
|
4
4
|
const msoListPattern = /(?:^|;)\s*mso-list\s*:\s*([^\s;]+)\s+level(\d+)\s+([^\s;]+)/i;
|
|
5
|
+
/**
|
|
6
|
+
* Elements whose HTML content model is inline-only - a real HTML parser
|
|
7
|
+
* never lets a <p>/<ul>/<ol>/<div> nest inside one of these (an outer <p>
|
|
8
|
+
* auto-closes first), so a nested Office mso-list paragraph or a
|
|
9
|
+
* declared-level <ul>/<ol> can never appear inside one. normalizeMsoLists/
|
|
10
|
+
* normalizeDeclaredLevelLists below recurse looking for exactly those two
|
|
11
|
+
* things - recursing into one of these tags is always wasted work. See
|
|
12
|
+
* docs/bugs/clipboard-paste-normalizer-quadratic-blowup-on-large-documents.md:
|
|
13
|
+
* on a real document with thousands of top-level paragraphs (the common
|
|
14
|
+
* case for a long pasted document with no lists at all), skipping this
|
|
15
|
+
* recursion is what turns an O(n) visit count with an accidentally
|
|
16
|
+
* document-size-scaled per-call cost back into genuinely linear work.
|
|
17
|
+
*/
|
|
18
|
+
const INLINE_LEAF_TAGS = new Set(["P", "SPAN", "B", "STRONG", "I", "EM", "U", "S", "A", "CODE", "BR", "IMG", "SUP", "SUB"]);
|
|
5
19
|
const unwrap = (element) => element.replaceWith(...Array.from(element.childNodes));
|
|
6
20
|
const normalizeTransparentContainers = (document) => {
|
|
7
21
|
const docsWrapper = document.body.querySelector('[id^="docs-internal-guid-"]');
|
|
@@ -71,7 +85,8 @@ const normalizeMsoLists = (document) => {
|
|
|
71
85
|
for (let index = 0; index < children.length;) {
|
|
72
86
|
const first = msoListEntry(children[index]);
|
|
73
87
|
if (!first) {
|
|
74
|
-
|
|
88
|
+
if (!INLINE_LEAF_TAGS.has(children[index].tagName))
|
|
89
|
+
visit(children[index]);
|
|
75
90
|
index += 1;
|
|
76
91
|
continue;
|
|
77
92
|
}
|
|
@@ -167,7 +182,8 @@ const normalizeDeclaredLevelLists = (document) => {
|
|
|
167
182
|
run[0].replaceWith(...roots);
|
|
168
183
|
run.slice(1).forEach((list) => list.remove());
|
|
169
184
|
}
|
|
170
|
-
Array.from(parent.children).forEach((child) =>
|
|
185
|
+
Array.from(parent.children).forEach((child) => { if (!INLINE_LEAF_TAGS.has(child.tagName))
|
|
186
|
+
visit(child); });
|
|
171
187
|
};
|
|
172
188
|
visit(document.body);
|
|
173
189
|
};
|
|
@@ -145,9 +145,20 @@ const inlineRun = (node, context) => {
|
|
|
145
145
|
};
|
|
146
146
|
const alignmentOf = (node) => typeof node.attrs?.align === "string" ? node.attrs.align : undefined;
|
|
147
147
|
const indentLevelOf = (node) => Number(node.attrs?.indentLevel) || 0;
|
|
148
|
+
/**
|
|
149
|
+
* Word's native "multiple" line spacing: `w:line` is in 240ths of a single
|
|
150
|
+
* line (240 = single/1.0, 360 = 1.5, 480 = double/2.0 - the same
|
|
151
|
+
* convention Word's own UI uses), `w:lineRule="auto"` is what makes it a
|
|
152
|
+
* multiplier rather than an exact fixed height (`"exact"`/`"atLeast"`
|
|
153
|
+
* would instead be a fixed point size, which is deliberately not what
|
|
154
|
+
* this feature stores - see schema.ts's lineHeightAttr doc comment on why
|
|
155
|
+
* a unitless multiplier was chosen over a fixed value).
|
|
156
|
+
*/
|
|
157
|
+
const lineHeightOf = (node) => Number(node.attrs?.lineHeight) || 0;
|
|
148
158
|
const paragraphProperties = (block, extra = "") => [
|
|
149
159
|
alignmentOf(block) ? `<w:jc w:val="${xmlEscape(alignmentOf(block))}"/>` : "",
|
|
150
160
|
indentLevelOf(block) ? `<w:ind w:left="${indentLevelOf(block) * 480}"/>` : "",
|
|
161
|
+
lineHeightOf(block) ? `<w:spacing w:line="${Math.round(lineHeightOf(block) * 240)}" w:lineRule="auto"/>` : "",
|
|
151
162
|
extra,
|
|
152
163
|
].join("");
|
|
153
164
|
const paragraphXml = (children, block, context, extraProperties = "") => {
|
|
@@ -236,6 +247,20 @@ const blockXml = (block, context, listLevel = 0) => {
|
|
|
236
247
|
return paragraphXml([{ ...block, type: "image" }], block, context);
|
|
237
248
|
if (block.type === "block_formula")
|
|
238
249
|
return paragraphXml([{ ...block, type: "formula" }], block, context);
|
|
250
|
+
// Previously fell through to the generic fallback below (an empty
|
|
251
|
+
// paragraph, since a divider has neither title nor src) - a horizontal
|
|
252
|
+
// line silently vanished on DOCX export with no visible trace at all.
|
|
253
|
+
// A bottom-bordered empty paragraph is exactly what Word's own
|
|
254
|
+
// Insert > Horizontal Line command produces.
|
|
255
|
+
if (block.type === "divider")
|
|
256
|
+
return '<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/></w:pBdr></w:pPr></w:p>';
|
|
257
|
+
// A real Word page break (matches Ctrl+Enter) - not a visual-only text
|
|
258
|
+
// marker. See atom/formats.ts's atomToDocx doc comment: mammoth's HTML
|
|
259
|
+
// conversion (this package's own DOCX import path) does not preserve
|
|
260
|
+
// this on re-import, confirmed via a real export/import round-trip test
|
|
261
|
+
// (format.test.ts) - export fidelity is real, import fidelity is not.
|
|
262
|
+
if (block.type === "page_break")
|
|
263
|
+
return '<w:p><w:r><w:br w:type="page"/></w:r></w:p>';
|
|
239
264
|
return paragraphXml([{ type: "text", text: String(block.attrs?.title || block.attrs?.src || "") }], block, context);
|
|
240
265
|
};
|
|
241
266
|
const numberingLevelXml = (level, format, text, font) => `<w:lvl w:ilvl="${level}"><w:start w:val="1"/><w:numFmt w:val="${format}"/><w:lvlText w:val="${text}"/><w:lvlJc w:val="left"/><w:pPr><w:tabs><w:tab w:val="num" w:pos="${720 + level * 360}"/></w:tabs><w:ind w:left="${720 + level * 360}" w:hanging="360"/></w:pPr>${font ? `<w:rPr><w:rFonts w:ascii="${font}" w:hAnsi="${font}"/></w:rPr>` : ""}</w:lvl>`;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { FeatureFormatCodec } from "./codec.js";
|
|
2
2
|
import { type FidelityFeature } from "./fidelity.js";
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
5
|
-
* gate
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* level or note
|
|
4
|
+
* The (feature x format) FeatureFormatCodec declarations Phase 9 SS3 gate 3
|
|
5
|
+
* requires (44 at that gate's close; "page-break" added later brings the
|
|
6
|
+
* total to 48). feature/format/fidelity/note are mechanically derived from
|
|
7
|
+
* builtInFormatFidelity (fidelity.ts) - that table is the single source of
|
|
8
|
+
* truth for fidelity claims; this file must never restate a level or note
|
|
9
|
+
* independently of it.
|
|
9
10
|
*
|
|
10
11
|
* parse/serialize are only attached where a genuine single-node function
|
|
11
12
|
* already exists, never a fabricated per-node wrapper around whole-document
|
|
12
13
|
* logic:
|
|
13
14
|
* - atom/formats.ts's atomTo{Html,Markdown,Docx,Pdf} and atomFromHtmlElement
|
|
14
|
-
* operate on one SmartElementNode at a time, so images-media
|
|
15
|
-
* get real wiring for all four formats
|
|
15
|
+
* operate on one SmartElementNode at a time, so images-media, formulas,
|
|
16
|
+
* and page-break get real wiring for all four formats.
|
|
16
17
|
* - marks/formats.ts's docxProperties and block/formats.ts's blockToDocxEntry
|
|
17
18
|
* (Phase 11 Tier 2, per docs/PHASE_9_CODEC_REFACTOR_SCOPE.md's recommended
|
|
18
19
|
* marks-then-blocks order) were already genuinely single-node - or, for
|
|
@@ -4,18 +4,19 @@ import { blockToDocxEntry } from "../block/formats.js";
|
|
|
4
4
|
import { builtInFormatFidelity } from "./fidelity.js";
|
|
5
5
|
const asAtom = (node) => node;
|
|
6
6
|
/**
|
|
7
|
-
* The
|
|
8
|
-
* gate
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* level or note
|
|
7
|
+
* The (feature x format) FeatureFormatCodec declarations Phase 9 SS3 gate 3
|
|
8
|
+
* requires (44 at that gate's close; "page-break" added later brings the
|
|
9
|
+
* total to 48). feature/format/fidelity/note are mechanically derived from
|
|
10
|
+
* builtInFormatFidelity (fidelity.ts) - that table is the single source of
|
|
11
|
+
* truth for fidelity claims; this file must never restate a level or note
|
|
12
|
+
* independently of it.
|
|
12
13
|
*
|
|
13
14
|
* parse/serialize are only attached where a genuine single-node function
|
|
14
15
|
* already exists, never a fabricated per-node wrapper around whole-document
|
|
15
16
|
* logic:
|
|
16
17
|
* - atom/formats.ts's atomTo{Html,Markdown,Docx,Pdf} and atomFromHtmlElement
|
|
17
|
-
* operate on one SmartElementNode at a time, so images-media
|
|
18
|
-
* get real wiring for all four formats
|
|
18
|
+
* operate on one SmartElementNode at a time, so images-media, formulas,
|
|
19
|
+
* and page-break get real wiring for all four formats.
|
|
19
20
|
* - marks/formats.ts's docxProperties and block/formats.ts's blockToDocxEntry
|
|
20
21
|
* (Phase 11 Tier 2, per docs/PHASE_9_CODEC_REFACTOR_SCOPE.md's recommended
|
|
21
22
|
* marks-then-blocks order) were already genuinely single-node - or, for
|
|
@@ -45,7 +46,7 @@ export const builtInFeatureFormatCodecs = builtInFormatFidelity.flatMap((contrac
|
|
|
45
46
|
fidelity: capability.level,
|
|
46
47
|
note: capability.note,
|
|
47
48
|
};
|
|
48
|
-
if (contract.feature === "images-media" || contract.feature === "formulas") {
|
|
49
|
+
if (contract.feature === "images-media" || contract.feature === "formulas" || contract.feature === "page-break") {
|
|
49
50
|
if (format === "html") {
|
|
50
51
|
return {
|
|
51
52
|
...base,
|
|
@@ -62,7 +63,7 @@ export const builtInFeatureFormatCodecs = builtInFormatFidelity.flatMap((contrac
|
|
|
62
63
|
if (format === "docx" && (contract.feature === "inline-marks" || contract.feature === "colors-fonts-sizes")) {
|
|
63
64
|
return { ...base, serialize: (node) => markRunDocxProperties(node) };
|
|
64
65
|
}
|
|
65
|
-
if (format === "docx" && (contract.feature === "headings-alignment" || contract.feature === "blockquote-code")) {
|
|
66
|
+
if (format === "docx" && (contract.feature === "headings-alignment" || contract.feature === "blockquote-code" || contract.feature === "line-height")) {
|
|
66
67
|
return { ...base, serialize: (node) => blockToDocxEntry(asAtom(node)) };
|
|
67
68
|
}
|
|
68
69
|
return base;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FormatFidelityLevel, FormatId } from "./codec.js";
|
|
2
2
|
export type FidelityFormat = FormatId;
|
|
3
3
|
export type FidelityLevel = FormatFidelityLevel;
|
|
4
|
-
export type FidelityFeature = "inline-marks" | "colors-fonts-sizes" | "headings-alignment" | "blockquote-code" | "lists" | "checklists" | "tables" | "links" | "images-media" | "formulas" | "special-characters";
|
|
4
|
+
export type FidelityFeature = "inline-marks" | "colors-fonts-sizes" | "headings-alignment" | "blockquote-code" | "lists" | "checklists" | "tables" | "links" | "images-media" | "formulas" | "special-characters" | "page-break" | "line-height";
|
|
5
5
|
export interface FormatFidelityCapability {
|
|
6
6
|
level: FidelityLevel;
|
|
7
7
|
note: string;
|
|
@@ -119,5 +119,23 @@ export const builtInFormatFidelity = [
|
|
|
119
119
|
pdf: capability("semantic", "Depends on font embedding and extractor Unicode maps."),
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
feature: "page-break",
|
|
124
|
+
formats: {
|
|
125
|
+
html: capability("full", "A marker element with `break-before: page`/`page-break-before: always` round-trips exactly, and genuinely paginates wherever the exported HTML is printed - including this package's own 'Save as PDF' (a real browser print of this HTML, see atom/formats.ts's atomToHtml doc comment)."),
|
|
126
|
+
markdown: capability("unsupported", "Markdown has no pagination concept. Exported as an inert `<!-- page break -->` HTML comment so the position is not silently dropped (matching this project's own history of atoms disappearing with no trace on markdown export); nothing parses this comment back into a page break on import, so this is genuinely `unsupported`, not merely undeclared."),
|
|
127
|
+
docx: capability("lossy", "A real native Word page break (`<w:br w:type=\"page\"/>`) is emitted on export - Word paginates correctly when the file is opened. Confirmed empirically (format.test.ts) that mammoth's HTML conversion (this package's own DOCX import path) drops the page break entirely on re-import, with no trace - export fidelity is real, import fidelity is not."),
|
|
128
|
+
pdf: capability("full", "This package's actual 'Save as PDF' is a real browser print of the same HTML export (react/src/adapters/pdfPrint.ts), so it inherits the html row's real pagination exactly - verified end to end via Playwright's own page.pdf() against the real print document, asserting a real page count increase. The separate atomToPdf per-node function (used only by this fidelity table's own test suite, not the real feature) is not the mechanism that produces this."),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
feature: "line-height",
|
|
133
|
+
formats: {
|
|
134
|
+
html: capability("full", "A unitless multiplier (matching CSS line-height's own unitless mode) round-trips exactly via a data-smart-line-height attribute plus an inline `line-height:` style declaration."),
|
|
135
|
+
markdown: capability("unsupported", "Markdown has no line-spacing concept at all. The paragraph's own text is preserved in full (line-height is metadata about how it's displayed, not content) - only the spacing value itself is dropped, matching this project's non-destructive-fallback discipline (nothing else about the paragraph is lost)."),
|
|
136
|
+
docx: capability("lossy", "Word's native \"multiple\" line spacing (`w:spacing w:line=\"...\" w:lineRule=\"auto\"`) is emitted on export - Word itself displays the correct spacing when the file is opened. Confirmed empirically (format.test.ts) that mammoth's HTML conversion (this package's own DOCX import path) drops the spacing value entirely on re-import - export fidelity is real, import fidelity is not, the same asymmetry already documented for page-break."),
|
|
137
|
+
pdf: capability("full", "This package's actual 'Save as PDF' is a real browser print of the same HTML export, so it inherits the html row's real, correctly-spaced layout exactly - verified against the real print document's computed styles, not assumed."),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
122
140
|
];
|
|
123
141
|
export const getFormatFidelity = (feature, format) => builtInFormatFidelity.find((entry) => entry.feature === feature).formats[format];
|
|
@@ -160,12 +160,35 @@ export const importPdfDocument = async (arrayBuffer) => {
|
|
|
160
160
|
return reconstructPdfPages(pages);
|
|
161
161
|
};
|
|
162
162
|
export const buildPdfPrintDocument = (document) => {
|
|
163
|
-
|
|
163
|
+
// Same static-HTML-consumer gap as Sootr's Web Preview (see docs/bugs/
|
|
164
|
+
// formula-not-rendered-in-static-html-consumers.md): this print window
|
|
165
|
+
// never runs the live editor's own KaTeX rendering against its DOM, so
|
|
166
|
+
// without renderFormulaHtml every formula/block_formula serializes as an
|
|
167
|
+
// empty <span>/<div> - present in the markup, invisible on the printed
|
|
168
|
+
// page and in the resulting PDF. Bakes real KaTeX HTML+MathML in instead,
|
|
169
|
+
// exactly like the Web Preview fix.
|
|
170
|
+
const html = serializeCanonicalListHtml(document, { clean: true, renderFormulaHtml: true });
|
|
164
171
|
return `<!doctype html>
|
|
165
172
|
<html lang="en">
|
|
166
173
|
<head>
|
|
167
174
|
<meta charset="utf-8">
|
|
168
175
|
<title>Smart RTE PDF Export</title>
|
|
176
|
+
<!--
|
|
177
|
+
KaTeX's rendered HTML (baked in above) is a set of unstyled <span>s
|
|
178
|
+
without this - it is a CSS framework, not self-describing markup. This
|
|
179
|
+
print window is a brand-new, isolated document (opened via
|
|
180
|
+
window.open + document.write - see react/src/adapters/pdfPrint.ts) that
|
|
181
|
+
shares none of the host page's own stylesheets, so it needs its own
|
|
182
|
+
copy regardless of whether the host has already loaded KaTeX's CSS for
|
|
183
|
+
the live editor. Matches this package's own documented setup
|
|
184
|
+
convention (README.md's KaTeX CDN <link>) rather than inventing a
|
|
185
|
+
second one - pinned to the exact version this package bundles
|
|
186
|
+
(package.json's "katex" dependency) so the JS that generated the
|
|
187
|
+
markup and the CSS that styles it never drift apart. Requires network
|
|
188
|
+
access at export time, the same as the live editor already requires
|
|
189
|
+
for its own KaTeX CSS.
|
|
190
|
+
-->
|
|
191
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.css">
|
|
169
192
|
<style>
|
|
170
193
|
@page { margin: 18mm; }
|
|
171
194
|
html, body { background: #fff; }
|
|
@@ -91,14 +91,18 @@ const effectiveStyle = (node, depth) => {
|
|
|
91
91
|
: undefined;
|
|
92
92
|
};
|
|
93
93
|
const blockAttributes = (node) => {
|
|
94
|
+
const lineHeight = Number(node.attrs?.lineHeight);
|
|
95
|
+
const hasLineHeight = Number.isFinite(lineHeight) && lineHeight > 0;
|
|
94
96
|
const declarations = [
|
|
95
97
|
typeof node.attrs?.htmlStyle === "string" ? node.attrs.htmlStyle.replace(/;\s*$/, "") : "",
|
|
96
98
|
typeof node.attrs?.align === "string" ? `text-align:${node.attrs.align}` : "",
|
|
97
99
|
Number(node.attrs?.indentLevel) > 0 ? `margin-inline-start:${Number(node.attrs?.indentLevel) * 2}em` : "",
|
|
100
|
+
hasLineHeight ? `line-height:${lineHeight}` : "",
|
|
98
101
|
].filter(Boolean);
|
|
99
102
|
return [
|
|
100
103
|
node.attrs?.align ? ` data-smart-align="${escapeHtml(node.attrs.align)}"` : "",
|
|
101
104
|
Number(node.attrs?.indentLevel) > 0 ? ` data-smart-indent="${escapeHtml(node.attrs?.indentLevel)}"` : "",
|
|
105
|
+
hasLineHeight ? ` data-smart-line-height="${escapeHtml(lineHeight)}"` : "",
|
|
102
106
|
declarations.length ? ` style="${escapeHtml(declarations.join(";"))}"` : "",
|
|
103
107
|
].join("");
|
|
104
108
|
};
|
|
@@ -109,7 +113,7 @@ const serializeBlock = (node, includeIds, listDepth = 0, tableCellA11y) => {
|
|
|
109
113
|
return raw.html;
|
|
110
114
|
}
|
|
111
115
|
const id = includeIds ? ` data-smart-id="${escapeHtml(node.id)}"` : "";
|
|
112
|
-
if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
|
|
116
|
+
if (["block_image", "block_formula", "video", "audio", "divider", "page_break"].includes(node.type))
|
|
113
117
|
return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
|
|
114
118
|
if (node.type === "paragraph" || node.type === "heading") {
|
|
115
119
|
const tag = node.type === "heading" ? `h${String(node.attrs?.level || 1)}` : "p";
|
|
@@ -385,6 +389,26 @@ const parsedBlockAttrs = (node) => {
|
|
|
385
389
|
attrs.align = align;
|
|
386
390
|
if (Number.isInteger(indent) && indent > 0)
|
|
387
391
|
attrs.indentLevel = indent;
|
|
392
|
+
// Deliberately NOT falling back to a bare CSS `line-height` value the way
|
|
393
|
+
// align/indent fall back to their own CSS equivalents above - confirmed
|
|
394
|
+
// directly against two real captured clipboard fixtures
|
|
395
|
+
// (google-docs-clipboard.clipboard.json, native-smart-rte-clipboard.
|
|
396
|
+
// clipboard.json) that a real-world `line-height` is very often an
|
|
397
|
+
// AMBIENT/inherited value (a whole document's own default line spacing,
|
|
398
|
+
// or - for the native fixture - literally this editor's own global
|
|
399
|
+
// `line-height: 1.6` from theme.ts, baked onto every element by the
|
|
400
|
+
// browser's clipboard serialization, not a value the user ever
|
|
401
|
+
// deliberately set on that one paragraph) rather than a genuine
|
|
402
|
+
// per-paragraph override. `text-align`/`margin-inline-start` do not have
|
|
403
|
+
// this problem in practice - browsers do not routinely bake an ambient
|
|
404
|
+
// default onto every paragraph's inline style the way they do for
|
|
405
|
+
// line-height. Recognizing the bare CSS value would silently attach a
|
|
406
|
+
// meaningless "explicit" lineHeight to every pasted paragraph. Only this
|
|
407
|
+
// app's own explicit `data-smart-line-height` marker (written exclusively
|
|
408
|
+
// when the user deliberately applied it via the toolbar) is trusted.
|
|
409
|
+
const lineHeight = Number(attr(node, "data-smart-line-height"));
|
|
410
|
+
if (Number.isFinite(lineHeight) && lineHeight >= 0.1 && lineHeight <= 10)
|
|
411
|
+
attrs.lineHeight = lineHeight;
|
|
388
412
|
return attrs;
|
|
389
413
|
};
|
|
390
414
|
const rawText = (node) => node.nodeName === "#text" ? node.value || ""
|
|
@@ -426,6 +450,12 @@ const parseBlock = (node) => {
|
|
|
426
450
|
return { type: "block_formula", id: generatedId(node, "formula"), attrs: { source: attr(node, "data-smart-formula") || rawText(node), notation: attr(node, "data-smart-notation") === "mathml" ? "mathml" : "latex" } };
|
|
427
451
|
if (tag === "hr")
|
|
428
452
|
return { type: "divider", id: generatedId(node, "divider") };
|
|
453
|
+
// No third-party HTML tag means "page break" the way <hr> means
|
|
454
|
+
// "horizontal rule" - only this app's own round-tripped export (the
|
|
455
|
+
// data-smart-type marker) is recognized, same as block_image/
|
|
456
|
+
// block_formula above.
|
|
457
|
+
if (declaredAtom === "page_break")
|
|
458
|
+
return { type: "page_break", id: generatedId(node, "pagebreak") };
|
|
429
459
|
// A third-party <img> sitting directly at block level (not wrapped in a
|
|
430
460
|
// <p>) - the ordinary shape for a standalone content photo on most real
|
|
431
461
|
// websites, e.g. a bare <img> between paragraphs or inside a <figure>
|
|
@@ -720,7 +750,18 @@ const parseMixedBlockContent = (nodes, blockTags) => {
|
|
|
720
750
|
inlineRun.push(node);
|
|
721
751
|
return;
|
|
722
752
|
}
|
|
723
|
-
|
|
753
|
+
// A bare third-party <div> (no data-smart-type) is genuinely
|
|
754
|
+
// meaningless wrapping and should be unwrapped - but this app's own
|
|
755
|
+
// round-tripped div-tagged atoms (block_formula, page_break; see
|
|
756
|
+
// atomToHtml) carry that marker precisely so they're recognized as
|
|
757
|
+
// opaque nodes, not content to flatten. Without this check, exporting
|
|
758
|
+
// and reimporting a block_formula (or page_break) silently deleted it
|
|
759
|
+
// outright: parseBlock never even saw the node, since this recursion
|
|
760
|
+
// handed off its (empty) childNodes instead - confirmed directly via
|
|
761
|
+
// `serializeCanonicalListHtml` -> `parseCanonicalListHtml` on a
|
|
762
|
+
// document containing nothing but a block_formula, which returned zero
|
|
763
|
+
// children back.
|
|
764
|
+
if (TRANSPARENT_CONTAINER_TAGS.includes(tag) && !attr(node, "data-smart-type")) {
|
|
724
765
|
flushInlineRun();
|
|
725
766
|
result.push(...parseMixedBlockContent(node.childNodes || [], blockTags));
|
|
726
767
|
return;
|
|
@@ -790,7 +831,7 @@ const markdownList = (list, depth) => (list.children || []).flatMap((child, inde
|
|
|
790
831
|
return [line, ...continuation, ...nested];
|
|
791
832
|
});
|
|
792
833
|
const markdownBlock = (node) => {
|
|
793
|
-
if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
|
|
834
|
+
if (["block_image", "block_formula", "video", "audio", "divider", "page_break"].includes(node.type))
|
|
794
835
|
return [atomToMarkdown(node)];
|
|
795
836
|
if (node.type === "list")
|
|
796
837
|
return markdownList(node, 0);
|
|
@@ -27,7 +27,7 @@ const renderFormulaInto = (element, source) => {
|
|
|
27
27
|
element.textContent = source;
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
|
-
const atomTypes = new Set(["image", "block_image", "formula", "block_formula", "video", "audio", "divider"]);
|
|
30
|
+
const atomTypes = new Set(["image", "block_image", "formula", "block_formula", "video", "audio", "divider", "page_break"]);
|
|
31
31
|
/**
|
|
32
32
|
* atom/lifecycle.ts writes a specific upload-failure reason to
|
|
33
33
|
* attrs.error (the host MediaProvider's own message, or a generic
|
|
@@ -117,6 +117,13 @@ export class FoundationSubtreeRenderer {
|
|
|
117
117
|
element.style.marginInlineStart = `${Number(node.attrs.indentLevel) * 2}em`;
|
|
118
118
|
else if (element.style.marginInlineStart)
|
|
119
119
|
element.style.removeProperty("margin-inline-start");
|
|
120
|
+
// Unitless, matching how the value is stored and validated (schema.ts's
|
|
121
|
+
// lineHeightAttr) - absence means "no override", the browser/font's own
|
|
122
|
+
// natural line-height, not a forced "1".
|
|
123
|
+
if (node.attrs?.lineHeight)
|
|
124
|
+
element.style.lineHeight = String(node.attrs.lineHeight);
|
|
125
|
+
else if (element.style.lineHeight)
|
|
126
|
+
element.style.removeProperty("line-height");
|
|
120
127
|
if (node.type === "code_block") {
|
|
121
128
|
const language = typeof node.attrs?.language === "string" && node.attrs.language.trim()
|
|
122
129
|
? node.attrs.language.trim()
|
|
@@ -527,7 +534,7 @@ export class FoundationSubtreeRenderer {
|
|
|
527
534
|
* the state through the normal diff path.
|
|
528
535
|
*/
|
|
529
536
|
installMediaDiagnostics(element, node) {
|
|
530
|
-
if (!atomTypes.has(node.type) || node.type === "formula" || node.type === "block_formula" || node.type === "divider")
|
|
537
|
+
if (!atomTypes.has(node.type) || node.type === "formula" || node.type === "block_formula" || node.type === "divider" || node.type === "page_break")
|
|
531
538
|
return;
|
|
532
539
|
if (element.hasAttribute("data-smart-media-events"))
|
|
533
540
|
return;
|