@scrider/formatter 1.2.0 → 1.3.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 +31 -2
- package/dist/index.cjs +103 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +102 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Schema, conversion and block handlers for rich-text content. HTML, Markdown, san
|
|
|
8
8
|
|
|
9
9
|
## Key Features
|
|
10
10
|
|
|
11
|
-
- **Schema** — extensible format registry (
|
|
11
|
+
- **Schema** — extensible format registry (32 built-in formats: inline, block, embed — including `softBreak` for Shift+Enter line breaks)
|
|
12
12
|
- **HTML conversion** — `deltaToHtml()` / `htmlToDelta()` with DOM adapters (browser + Node.js)
|
|
13
13
|
- **Markdown conversion** — `deltaToMarkdown()` / `markdownToDelta()` (GFM, math, footnotes)
|
|
14
14
|
- **Block handlers** — tables, footnotes, alerts, columns, inline-box
|
|
@@ -65,7 +65,7 @@ const delta = htmlToDelta(html, { registry });
|
|
|
65
65
|
```typescript
|
|
66
66
|
import { Registry, createDefaultRegistry, BlockHandlerRegistry } from '@scrider/formatter';
|
|
67
67
|
|
|
68
|
-
const registry = createDefaultRegistry(); //
|
|
68
|
+
const registry = createDefaultRegistry(); // 32 built-in formats
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
### HTML Conversion
|
|
@@ -86,6 +86,35 @@ deltaToMarkdown(delta, options?) // Delta → Markdown string
|
|
|
86
86
|
await markdownToDelta(markdown, options?) // Markdown string → Delta (async)
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Soft Line Break (`softBreak` embed)
|
|
90
|
+
|
|
91
|
+
A `Shift+Enter` style line break that does **not** split the containing block. Stored in Delta as `{ insert: { softBreak: true } }` and round-tripped consistently across all three layers:
|
|
92
|
+
|
|
93
|
+
| Direction | Encoding |
|
|
94
|
+
|-----------|----------|
|
|
95
|
+
| HTML | `<br data-scrider-embed>` (the marker disambiguates it from the `<br>` placeholder inside an empty paragraph) |
|
|
96
|
+
| Markdown | `" \n"` by default — GFM hard break; switch to inline `<br>` via `deltaToMarkdown(delta, { softBreakStyle: 'html' })` |
|
|
97
|
+
|
|
98
|
+
`htmlToDelta` also recognises bare `<br>` between content (e.g. `<p>foo<br>bar</p>`) as a soft break, while keeping the leading / placeholder shapes (`<p><br></p>`, `<p><br>foo</p>`) as regular newlines for backward compatibility.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Delta, deltaToHtml, deltaToMarkdown } from '@scrider/formatter';
|
|
102
|
+
|
|
103
|
+
const doc = new Delta()
|
|
104
|
+
.insert('hello')
|
|
105
|
+
.insert({ softBreak: true })
|
|
106
|
+
.insert('world\n');
|
|
107
|
+
|
|
108
|
+
deltaToHtml(doc);
|
|
109
|
+
// → '<p>hello<br data-scrider-embed>world</p>'
|
|
110
|
+
|
|
111
|
+
deltaToMarkdown(doc);
|
|
112
|
+
// → 'hello \nworld'
|
|
113
|
+
|
|
114
|
+
deltaToMarkdown(doc, { softBreakStyle: 'html' });
|
|
115
|
+
// → 'hello<br>world'
|
|
116
|
+
```
|
|
117
|
+
|
|
89
118
|
### Sanitization
|
|
90
119
|
|
|
91
120
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -96,6 +96,7 @@ __export(index_exports, {
|
|
|
96
96
|
sizeFormat: () => sizeFormat,
|
|
97
97
|
slugify: () => slugify,
|
|
98
98
|
slugifyWithDedup: () => slugifyWithDedup,
|
|
99
|
+
softBreakFormat: () => softBreakFormat,
|
|
99
100
|
strikeFormat: () => strikeFormat,
|
|
100
101
|
subscriptFormat: () => subscriptFormat,
|
|
101
102
|
superscriptFormat: () => superscriptFormat,
|
|
@@ -2020,7 +2021,13 @@ var EMBED_RENDERERS = {
|
|
|
2020
2021
|
const id = typeof value === "string" ? value : String(value);
|
|
2021
2022
|
return `<sup class="footnote-ref"><a href="#fn-${escapeHtml(id)}" id="fnref-${escapeHtml(id)}">[${escapeHtml(id)}]</a></sup>`;
|
|
2022
2023
|
},
|
|
2023
|
-
divider: () => "<hr>"
|
|
2024
|
+
divider: () => "<hr>",
|
|
2025
|
+
// Soft line break (Shift+Enter equivalent). Emitted with an explicit
|
|
2026
|
+
// `data-scrider-embed` marker so that html-to-delta can distinguish this
|
|
2027
|
+
// embed from the placeholder `<br>` that appears inside an empty
|
|
2028
|
+
// paragraph (`<p><br></p>`) without relying solely on positional
|
|
2029
|
+
// heuristics. See `soft-break.ts` for the format definition.
|
|
2030
|
+
softBreak: () => "<br data-scrider-embed>"
|
|
2024
2031
|
};
|
|
2025
2032
|
var TAG_TO_INLINE_FORMAT = {
|
|
2026
2033
|
strong: { format: "bold", value: true },
|
|
@@ -2258,6 +2265,33 @@ var imageFormat = {
|
|
|
2258
2265
|
}
|
|
2259
2266
|
};
|
|
2260
2267
|
|
|
2268
|
+
// src/schema/formats/embed/soft-break.ts
|
|
2269
|
+
var softBreakFormat = {
|
|
2270
|
+
name: "softBreak",
|
|
2271
|
+
scope: "embed",
|
|
2272
|
+
normalize(value) {
|
|
2273
|
+
return !!value;
|
|
2274
|
+
},
|
|
2275
|
+
validate(value) {
|
|
2276
|
+
return value === true;
|
|
2277
|
+
},
|
|
2278
|
+
render() {
|
|
2279
|
+
return "<br data-scrider-embed>";
|
|
2280
|
+
},
|
|
2281
|
+
match(element) {
|
|
2282
|
+
if (element.tagName.toLowerCase() !== "br") return null;
|
|
2283
|
+
if (!element.hasAttribute("data-scrider-embed")) return null;
|
|
2284
|
+
return { value: true };
|
|
2285
|
+
}
|
|
2286
|
+
// NB: Markdown rendering is intentionally NOT implemented on the format
|
|
2287
|
+
// itself. The choice between `" \n"` (GFM spaces) and inline `<br>`
|
|
2288
|
+
// depends on the caller-provided `softBreakStyle` option on
|
|
2289
|
+
// `deltaToMarkdown`, so the converter handles it as a built-in special
|
|
2290
|
+
// case instead of going through `Format.toMarkdown`. The Markdown side
|
|
2291
|
+
// of the round-trip is symmetric: `markdownToDelta` recognises both
|
|
2292
|
+
// `break` AST nodes and inline `<br>` HTML and emits this embed.
|
|
2293
|
+
};
|
|
2294
|
+
|
|
2261
2295
|
// src/schema/formats/embed/video.ts
|
|
2262
2296
|
var videoFormat = {
|
|
2263
2297
|
name: "video",
|
|
@@ -2360,6 +2394,7 @@ var defaultEmbedFormats = [
|
|
|
2360
2394
|
videoFormat,
|
|
2361
2395
|
formulaFormat,
|
|
2362
2396
|
dividerFormat,
|
|
2397
|
+
softBreakFormat,
|
|
2363
2398
|
blockFormat,
|
|
2364
2399
|
footnoteRefFormat
|
|
2365
2400
|
];
|
|
@@ -3190,7 +3225,12 @@ function htmlToDelta(html, options = {}) {
|
|
|
3190
3225
|
return;
|
|
3191
3226
|
}
|
|
3192
3227
|
if (tagName === "br") {
|
|
3193
|
-
|
|
3228
|
+
const hasMarker = node.hasAttribute("data-scrider-embed");
|
|
3229
|
+
if (hasMarker || hasMeaningfulPrevSibling(node)) {
|
|
3230
|
+
context.pushEmbed({ softBreak: true });
|
|
3231
|
+
} else {
|
|
3232
|
+
context.pushNewline();
|
|
3233
|
+
}
|
|
3194
3234
|
return;
|
|
3195
3235
|
}
|
|
3196
3236
|
processChildren(node);
|
|
@@ -3624,6 +3664,23 @@ function normalizeText(text, pendingText, atLineStart) {
|
|
|
3624
3664
|
}
|
|
3625
3665
|
return text;
|
|
3626
3666
|
}
|
|
3667
|
+
function hasMeaningfulPrevSibling(brNode) {
|
|
3668
|
+
const parent = brNode.parentNode;
|
|
3669
|
+
if (!parent) return false;
|
|
3670
|
+
const children = parent.childNodes;
|
|
3671
|
+
for (let i = 0; i < children.length; i++) {
|
|
3672
|
+
const child = children[i];
|
|
3673
|
+
if (!child) continue;
|
|
3674
|
+
if (child === brNode) return false;
|
|
3675
|
+
if (child.nodeType === NODE_TYPE.TEXT_NODE) {
|
|
3676
|
+
const text = child.textContent ?? "";
|
|
3677
|
+
if (text.trim().length > 0) return true;
|
|
3678
|
+
} else if (isElement(child)) {
|
|
3679
|
+
return true;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
return false;
|
|
3683
|
+
}
|
|
3627
3684
|
function findTagHandler(handlers, element, tagName) {
|
|
3628
3685
|
const className = element.getAttribute("class");
|
|
3629
3686
|
if (className) {
|
|
@@ -3760,6 +3817,7 @@ function deltaToMarkdown(delta, options = {}) {
|
|
|
3760
3817
|
blockHandlers,
|
|
3761
3818
|
prettyHtml = false,
|
|
3762
3819
|
registry,
|
|
3820
|
+
softBreakStyle = "spaces",
|
|
3763
3821
|
trimTrailingNewlines = false
|
|
3764
3822
|
} = options;
|
|
3765
3823
|
const useLatexDelimiters = mathSyntax === "latex";
|
|
@@ -3776,7 +3834,9 @@ function deltaToMarkdown(delta, options = {}) {
|
|
|
3776
3834
|
const isBlockquote = !!attrs.blockquote;
|
|
3777
3835
|
if (typeof attrs["table-row"] === "number" && typeof attrs["table-col"] === "number") {
|
|
3778
3836
|
const tableLines = collectTableLines2(lines, i);
|
|
3779
|
-
result.push(
|
|
3837
|
+
result.push(
|
|
3838
|
+
renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
|
|
3839
|
+
);
|
|
3780
3840
|
result.push("");
|
|
3781
3841
|
i += tableLines.length - 1;
|
|
3782
3842
|
lastListType = null;
|
|
@@ -3792,7 +3852,16 @@ function deltaToMarkdown(delta, options = {}) {
|
|
|
3792
3852
|
const codeLines = collectCodeBlock(lines, i);
|
|
3793
3853
|
const language = getCodeBlockLanguage2(attrs);
|
|
3794
3854
|
const code = codeLines.map(
|
|
3795
|
-
(l) => renderLineContent2(
|
|
3855
|
+
(l) => renderLineContent2(
|
|
3856
|
+
l.ops,
|
|
3857
|
+
embedRenderers,
|
|
3858
|
+
true,
|
|
3859
|
+
false,
|
|
3860
|
+
blockHandlers,
|
|
3861
|
+
false,
|
|
3862
|
+
registry,
|
|
3863
|
+
softBreakStyle
|
|
3864
|
+
)
|
|
3796
3865
|
).join("\n");
|
|
3797
3866
|
if (language === "math") {
|
|
3798
3867
|
if (mathBlock === false) {
|
|
@@ -3842,7 +3911,8 @@ ${code}
|
|
|
3842
3911
|
useLatexDelimiters,
|
|
3843
3912
|
blockHandlers,
|
|
3844
3913
|
prettyHtml,
|
|
3845
|
-
registry
|
|
3914
|
+
registry,
|
|
3915
|
+
softBreakStyle
|
|
3846
3916
|
);
|
|
3847
3917
|
if (!content && !hasBlockFormat(attrs)) {
|
|
3848
3918
|
result.push(preserveEmptyLines ? "<br>" : "");
|
|
@@ -3944,7 +4014,7 @@ function collectTableLines2(lines, startIndex) {
|
|
|
3944
4014
|
}
|
|
3945
4015
|
return result;
|
|
3946
4016
|
}
|
|
3947
|
-
function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = false, registry) {
|
|
4017
|
+
function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = false, registry, softBreakStyle = "spaces") {
|
|
3948
4018
|
const rows = /* @__PURE__ */ new Map();
|
|
3949
4019
|
for (const line of tableLines) {
|
|
3950
4020
|
const attrs = line.attributes;
|
|
@@ -3981,7 +4051,9 @@ function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = fa
|
|
|
3981
4051
|
const mdLines = [];
|
|
3982
4052
|
if (headerRows.length > 0) {
|
|
3983
4053
|
for (const [, row] of headerRows) {
|
|
3984
|
-
mdLines.push(
|
|
4054
|
+
mdLines.push(
|
|
4055
|
+
renderMdRow(row.cells, maxCol, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
|
|
4056
|
+
);
|
|
3985
4057
|
}
|
|
3986
4058
|
mdLines.push(renderMdSeparator(maxCol, colAligns));
|
|
3987
4059
|
} else {
|
|
@@ -3989,15 +4061,19 @@ function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = fa
|
|
|
3989
4061
|
for (let col = 0; col <= maxCol; col++) {
|
|
3990
4062
|
emptyRow.set(col, { ops: [] });
|
|
3991
4063
|
}
|
|
3992
|
-
mdLines.push(
|
|
4064
|
+
mdLines.push(
|
|
4065
|
+
renderMdRow(emptyRow, maxCol, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
|
|
4066
|
+
);
|
|
3993
4067
|
mdLines.push(renderMdSeparator(maxCol, colAligns));
|
|
3994
4068
|
}
|
|
3995
4069
|
for (const [, row] of bodyRows) {
|
|
3996
|
-
mdLines.push(
|
|
4070
|
+
mdLines.push(
|
|
4071
|
+
renderMdRow(row.cells, maxCol, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
|
|
4072
|
+
);
|
|
3997
4073
|
}
|
|
3998
4074
|
return mdLines.join("\n");
|
|
3999
4075
|
}
|
|
4000
|
-
function renderMdRow(cells, maxCol, embedRenderers, useLatexDelimiters = false, registry) {
|
|
4076
|
+
function renderMdRow(cells, maxCol, embedRenderers, useLatexDelimiters = false, registry, softBreakStyle = "spaces") {
|
|
4001
4077
|
const parts = [];
|
|
4002
4078
|
for (let col = 0; col <= maxCol; col++) {
|
|
4003
4079
|
const cell = cells.get(col);
|
|
@@ -4008,7 +4084,10 @@ function renderMdRow(cells, maxCol, embedRenderers, useLatexDelimiters = false,
|
|
|
4008
4084
|
useLatexDelimiters,
|
|
4009
4085
|
void 0,
|
|
4010
4086
|
false,
|
|
4011
|
-
registry
|
|
4087
|
+
registry,
|
|
4088
|
+
softBreakStyle,
|
|
4089
|
+
true
|
|
4090
|
+
// inTableCell — softBreak must use <br>, never " \n"
|
|
4012
4091
|
) : "";
|
|
4013
4092
|
parts.push(content.replace(/\|/g, "\\|"));
|
|
4014
4093
|
}
|
|
@@ -4037,7 +4116,7 @@ function getCodeBlockLanguage2(attributes) {
|
|
|
4037
4116
|
}
|
|
4038
4117
|
return void 0;
|
|
4039
4118
|
}
|
|
4040
|
-
function renderLineContent2(ops, embedRenderers, inCodeBlock, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry) {
|
|
4119
|
+
function renderLineContent2(ops, embedRenderers, inCodeBlock, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry, softBreakStyle = "spaces", inTableCell = false) {
|
|
4041
4120
|
let result = "";
|
|
4042
4121
|
for (const op of ops) {
|
|
4043
4122
|
const attrs = op.attributes;
|
|
@@ -4057,7 +4136,9 @@ function renderLineContent2(ops, embedRenderers, inCodeBlock, useLatexDelimiters
|
|
|
4057
4136
|
useLatexDelimiters,
|
|
4058
4137
|
blockHandlers,
|
|
4059
4138
|
prettyHtml,
|
|
4060
|
-
registry
|
|
4139
|
+
registry,
|
|
4140
|
+
softBreakStyle,
|
|
4141
|
+
inTableCell
|
|
4061
4142
|
);
|
|
4062
4143
|
}
|
|
4063
4144
|
}
|
|
@@ -4091,13 +4172,17 @@ function renderInlineText2(text, attributes) {
|
|
|
4091
4172
|
}
|
|
4092
4173
|
return result;
|
|
4093
4174
|
}
|
|
4094
|
-
function renderEmbed2(embed, attributes, customRenderers, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry) {
|
|
4175
|
+
function renderEmbed2(embed, attributes, customRenderers, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry, softBreakStyle = "spaces", inTableCell = false) {
|
|
4095
4176
|
const entries = Object.entries(embed);
|
|
4096
4177
|
if (entries.length === 0) return "";
|
|
4097
4178
|
const firstEntry = entries[0];
|
|
4098
4179
|
if (!firstEntry) return "";
|
|
4099
4180
|
const embedType = firstEntry[0];
|
|
4100
4181
|
const embedValue = firstEntry[1];
|
|
4182
|
+
if (embedType === "softBreak") {
|
|
4183
|
+
if (inTableCell) return "<br>";
|
|
4184
|
+
return softBreakStyle === "html" ? "<br>" : " \n";
|
|
4185
|
+
}
|
|
4101
4186
|
if (embedType === "block" && blockHandlers) {
|
|
4102
4187
|
const blockData = embedValue;
|
|
4103
4188
|
if (blockData && typeof blockData.type === "string") {
|
|
@@ -4586,7 +4671,7 @@ function astToDelta(tree, customHandlers, mathBlock, mermaidBlock, plantumlBlock
|
|
|
4586
4671
|
footnoteDefinitions.set(node.identifier ?? "", node);
|
|
4587
4672
|
break;
|
|
4588
4673
|
case "break":
|
|
4589
|
-
context.
|
|
4674
|
+
context.pushEmbed({ softBreak: true });
|
|
4590
4675
|
break;
|
|
4591
4676
|
case "html": {
|
|
4592
4677
|
const htmlContent = node.value ?? "";
|
|
@@ -4892,8 +4977,8 @@ function astToDelta(tree, customHandlers, mathBlock, mermaidBlock, plantumlBlock
|
|
|
4892
4977
|
context.pushNewline();
|
|
4893
4978
|
return;
|
|
4894
4979
|
}
|
|
4895
|
-
if (/^<br\
|
|
4896
|
-
context.
|
|
4980
|
+
if (/^<br\b[^>]*\/?>$/i.test(html)) {
|
|
4981
|
+
context.pushEmbed({ softBreak: true });
|
|
4897
4982
|
return;
|
|
4898
4983
|
}
|
|
4899
4984
|
}
|
|
@@ -5044,6 +5129,7 @@ function extractTableRegion(ops, hintOpIdx) {
|
|
|
5044
5129
|
sizeFormat,
|
|
5045
5130
|
slugify,
|
|
5046
5131
|
slugifyWithDedup,
|
|
5132
|
+
softBreakFormat,
|
|
5047
5133
|
strikeFormat,
|
|
5048
5134
|
subscriptFormat,
|
|
5049
5135
|
superscriptFormat,
|