@scrider/formatter 1.1.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/dist/index.js CHANGED
@@ -1915,7 +1915,13 @@ var EMBED_RENDERERS = {
1915
1915
  const id = typeof value === "string" ? value : String(value);
1916
1916
  return `<sup class="footnote-ref"><a href="#fn-${escapeHtml(id)}" id="fnref-${escapeHtml(id)}">[${escapeHtml(id)}]</a></sup>`;
1917
1917
  },
1918
- divider: () => "<hr>"
1918
+ divider: () => "<hr>",
1919
+ // Soft line break (Shift+Enter equivalent). Emitted with an explicit
1920
+ // `data-scrider-embed` marker so that html-to-delta can distinguish this
1921
+ // embed from the placeholder `<br>` that appears inside an empty
1922
+ // paragraph (`<p><br></p>`) without relying solely on positional
1923
+ // heuristics. See `soft-break.ts` for the format definition.
1924
+ softBreak: () => "<br data-scrider-embed>"
1919
1925
  };
1920
1926
  var TAG_TO_INLINE_FORMAT = {
1921
1927
  strong: { format: "bold", value: true },
@@ -2153,6 +2159,33 @@ var imageFormat = {
2153
2159
  }
2154
2160
  };
2155
2161
 
2162
+ // src/schema/formats/embed/soft-break.ts
2163
+ var softBreakFormat = {
2164
+ name: "softBreak",
2165
+ scope: "embed",
2166
+ normalize(value) {
2167
+ return !!value;
2168
+ },
2169
+ validate(value) {
2170
+ return value === true;
2171
+ },
2172
+ render() {
2173
+ return "<br data-scrider-embed>";
2174
+ },
2175
+ match(element) {
2176
+ if (element.tagName.toLowerCase() !== "br") return null;
2177
+ if (!element.hasAttribute("data-scrider-embed")) return null;
2178
+ return { value: true };
2179
+ }
2180
+ // NB: Markdown rendering is intentionally NOT implemented on the format
2181
+ // itself. The choice between `" \n"` (GFM spaces) and inline `<br>`
2182
+ // depends on the caller-provided `softBreakStyle` option on
2183
+ // `deltaToMarkdown`, so the converter handles it as a built-in special
2184
+ // case instead of going through `Format.toMarkdown`. The Markdown side
2185
+ // of the round-trip is symmetric: `markdownToDelta` recognises both
2186
+ // `break` AST nodes and inline `<br>` HTML and emits this embed.
2187
+ };
2188
+
2156
2189
  // src/schema/formats/embed/video.ts
2157
2190
  var videoFormat = {
2158
2191
  name: "video",
@@ -2255,6 +2288,7 @@ var defaultEmbedFormats = [
2255
2288
  videoFormat,
2256
2289
  formulaFormat,
2257
2290
  dividerFormat,
2291
+ softBreakFormat,
2258
2292
  blockFormat,
2259
2293
  footnoteRefFormat
2260
2294
  ];
@@ -3085,7 +3119,12 @@ function htmlToDelta(html, options = {}) {
3085
3119
  return;
3086
3120
  }
3087
3121
  if (tagName === "br") {
3088
- context.pushNewline();
3122
+ const hasMarker = node.hasAttribute("data-scrider-embed");
3123
+ if (hasMarker || hasMeaningfulPrevSibling(node)) {
3124
+ context.pushEmbed({ softBreak: true });
3125
+ } else {
3126
+ context.pushNewline();
3127
+ }
3089
3128
  return;
3090
3129
  }
3091
3130
  processChildren(node);
@@ -3519,6 +3558,23 @@ function normalizeText(text, pendingText, atLineStart) {
3519
3558
  }
3520
3559
  return text;
3521
3560
  }
3561
+ function hasMeaningfulPrevSibling(brNode) {
3562
+ const parent = brNode.parentNode;
3563
+ if (!parent) return false;
3564
+ const children = parent.childNodes;
3565
+ for (let i = 0; i < children.length; i++) {
3566
+ const child = children[i];
3567
+ if (!child) continue;
3568
+ if (child === brNode) return false;
3569
+ if (child.nodeType === NODE_TYPE.TEXT_NODE) {
3570
+ const text = child.textContent ?? "";
3571
+ if (text.trim().length > 0) return true;
3572
+ } else if (isElement(child)) {
3573
+ return true;
3574
+ }
3575
+ }
3576
+ return false;
3577
+ }
3522
3578
  function findTagHandler(handlers, element, tagName) {
3523
3579
  const className = element.getAttribute("class");
3524
3580
  if (className) {
@@ -3654,7 +3710,9 @@ function deltaToMarkdown(delta, options = {}) {
3654
3710
  embedRenderers = {},
3655
3711
  blockHandlers,
3656
3712
  prettyHtml = false,
3657
- registry
3713
+ registry,
3714
+ softBreakStyle = "spaces",
3715
+ trimTrailingNewlines = false
3658
3716
  } = options;
3659
3717
  const useLatexDelimiters = mathSyntax === "latex";
3660
3718
  const lines = splitIntoLines2(delta.ops);
@@ -3670,7 +3728,9 @@ function deltaToMarkdown(delta, options = {}) {
3670
3728
  const isBlockquote = !!attrs.blockquote;
3671
3729
  if (typeof attrs["table-row"] === "number" && typeof attrs["table-col"] === "number") {
3672
3730
  const tableLines = collectTableLines2(lines, i);
3673
- result.push(renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters, registry));
3731
+ result.push(
3732
+ renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
3733
+ );
3674
3734
  result.push("");
3675
3735
  i += tableLines.length - 1;
3676
3736
  lastListType = null;
@@ -3686,7 +3746,16 @@ function deltaToMarkdown(delta, options = {}) {
3686
3746
  const codeLines = collectCodeBlock(lines, i);
3687
3747
  const language = getCodeBlockLanguage2(attrs);
3688
3748
  const code = codeLines.map(
3689
- (l) => renderLineContent2(l.ops, embedRenderers, true, false, blockHandlers, false, registry)
3749
+ (l) => renderLineContent2(
3750
+ l.ops,
3751
+ embedRenderers,
3752
+ true,
3753
+ false,
3754
+ blockHandlers,
3755
+ false,
3756
+ registry,
3757
+ softBreakStyle
3758
+ )
3690
3759
  ).join("\n");
3691
3760
  if (language === "math") {
3692
3761
  if (mathBlock === false) {
@@ -3736,7 +3805,8 @@ ${code}
3736
3805
  useLatexDelimiters,
3737
3806
  blockHandlers,
3738
3807
  prettyHtml,
3739
- registry
3808
+ registry,
3809
+ softBreakStyle
3740
3810
  );
3741
3811
  if (!content && !hasBlockFormat(attrs)) {
3742
3812
  result.push(preserveEmptyLines ? "<br>" : "");
@@ -3752,7 +3822,8 @@ ${code}
3752
3822
  lastIndent = indent;
3753
3823
  lastWasBlockquote = isBlockquote;
3754
3824
  }
3755
- return result.join("\n");
3825
+ const md = result.join("\n");
3826
+ return trimTrailingNewlines ? md.replace(/\n+$/, "") : md;
3756
3827
  }
3757
3828
  function hasBlockFormat(attrs) {
3758
3829
  return !!(attrs.header || attrs.list || attrs.blockquote || attrs["code-block"] || attrs.align || attrs.indent);
@@ -3837,7 +3908,7 @@ function collectTableLines2(lines, startIndex) {
3837
3908
  }
3838
3909
  return result;
3839
3910
  }
3840
- function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = false, registry) {
3911
+ function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = false, registry, softBreakStyle = "spaces") {
3841
3912
  const rows = /* @__PURE__ */ new Map();
3842
3913
  for (const line of tableLines) {
3843
3914
  const attrs = line.attributes;
@@ -3874,7 +3945,9 @@ function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = fa
3874
3945
  const mdLines = [];
3875
3946
  if (headerRows.length > 0) {
3876
3947
  for (const [, row] of headerRows) {
3877
- mdLines.push(renderMdRow(row.cells, maxCol, embedRenderers, useLatexDelimiters, registry));
3948
+ mdLines.push(
3949
+ renderMdRow(row.cells, maxCol, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
3950
+ );
3878
3951
  }
3879
3952
  mdLines.push(renderMdSeparator(maxCol, colAligns));
3880
3953
  } else {
@@ -3882,15 +3955,19 @@ function renderMarkdownTable(tableLines, embedRenderers, useLatexDelimiters = fa
3882
3955
  for (let col = 0; col <= maxCol; col++) {
3883
3956
  emptyRow.set(col, { ops: [] });
3884
3957
  }
3885
- mdLines.push(renderMdRow(emptyRow, maxCol, embedRenderers, useLatexDelimiters, registry));
3958
+ mdLines.push(
3959
+ renderMdRow(emptyRow, maxCol, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
3960
+ );
3886
3961
  mdLines.push(renderMdSeparator(maxCol, colAligns));
3887
3962
  }
3888
3963
  for (const [, row] of bodyRows) {
3889
- mdLines.push(renderMdRow(row.cells, maxCol, embedRenderers, useLatexDelimiters, registry));
3964
+ mdLines.push(
3965
+ renderMdRow(row.cells, maxCol, embedRenderers, useLatexDelimiters, registry, softBreakStyle)
3966
+ );
3890
3967
  }
3891
3968
  return mdLines.join("\n");
3892
3969
  }
3893
- function renderMdRow(cells, maxCol, embedRenderers, useLatexDelimiters = false, registry) {
3970
+ function renderMdRow(cells, maxCol, embedRenderers, useLatexDelimiters = false, registry, softBreakStyle = "spaces") {
3894
3971
  const parts = [];
3895
3972
  for (let col = 0; col <= maxCol; col++) {
3896
3973
  const cell = cells.get(col);
@@ -3901,7 +3978,10 @@ function renderMdRow(cells, maxCol, embedRenderers, useLatexDelimiters = false,
3901
3978
  useLatexDelimiters,
3902
3979
  void 0,
3903
3980
  false,
3904
- registry
3981
+ registry,
3982
+ softBreakStyle,
3983
+ true
3984
+ // inTableCell — softBreak must use <br>, never " \n"
3905
3985
  ) : "";
3906
3986
  parts.push(content.replace(/\|/g, "\\|"));
3907
3987
  }
@@ -3930,7 +4010,7 @@ function getCodeBlockLanguage2(attributes) {
3930
4010
  }
3931
4011
  return void 0;
3932
4012
  }
3933
- function renderLineContent2(ops, embedRenderers, inCodeBlock, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry) {
4013
+ function renderLineContent2(ops, embedRenderers, inCodeBlock, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry, softBreakStyle = "spaces", inTableCell = false) {
3934
4014
  let result = "";
3935
4015
  for (const op of ops) {
3936
4016
  const attrs = op.attributes;
@@ -3950,7 +4030,9 @@ function renderLineContent2(ops, embedRenderers, inCodeBlock, useLatexDelimiters
3950
4030
  useLatexDelimiters,
3951
4031
  blockHandlers,
3952
4032
  prettyHtml,
3953
- registry
4033
+ registry,
4034
+ softBreakStyle,
4035
+ inTableCell
3954
4036
  );
3955
4037
  }
3956
4038
  }
@@ -3984,13 +4066,17 @@ function renderInlineText2(text, attributes) {
3984
4066
  }
3985
4067
  return result;
3986
4068
  }
3987
- function renderEmbed2(embed, attributes, customRenderers, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry) {
4069
+ function renderEmbed2(embed, attributes, customRenderers, useLatexDelimiters = false, blockHandlers, prettyHtml = false, registry, softBreakStyle = "spaces", inTableCell = false) {
3988
4070
  const entries = Object.entries(embed);
3989
4071
  if (entries.length === 0) return "";
3990
4072
  const firstEntry = entries[0];
3991
4073
  if (!firstEntry) return "";
3992
4074
  const embedType = firstEntry[0];
3993
4075
  const embedValue = firstEntry[1];
4076
+ if (embedType === "softBreak") {
4077
+ if (inTableCell) return "<br>";
4078
+ return softBreakStyle === "html" ? "<br>" : " \n";
4079
+ }
3994
4080
  if (embedType === "block" && blockHandlers) {
3995
4081
  const blockData = embedValue;
3996
4082
  if (blockData && typeof blockData.type === "string") {
@@ -4141,6 +4227,8 @@ var remarkGfm = null;
4141
4227
  var remarkMath = null;
4142
4228
  var unified = null;
4143
4229
  function isRemarkAvailable() {
4230
+ if (unified && remarkParse) return true;
4231
+ if (typeof __require === "undefined") return false;
4144
4232
  try {
4145
4233
  __require.resolve("unified");
4146
4234
  __require.resolve("remark-parse");
@@ -4149,8 +4237,8 @@ function isRemarkAvailable() {
4149
4237
  return false;
4150
4238
  }
4151
4239
  }
4152
- async function loadRemark() {
4153
- if (unified) return;
4240
+ async function preloadRemark() {
4241
+ if (unified && remarkParse && remarkGfm) return true;
4154
4242
  try {
4155
4243
  const [unifiedMod, remarkParseMod, remarkGfmMod] = await Promise.all([
4156
4244
  import("unified"),
@@ -4161,15 +4249,16 @@ async function loadRemark() {
4161
4249
  remarkParse = remarkParseMod.default;
4162
4250
  remarkGfm = remarkGfmMod.default;
4163
4251
  } catch {
4164
- throw new Error(
4165
- "remark is not installed. Install with: pnpm add unified remark-parse remark-gfm"
4166
- );
4252
+ return false;
4167
4253
  }
4168
- try {
4169
- const remarkMathMod = await import("remark-math");
4170
- remarkMath = remarkMathMod.default;
4171
- } catch {
4254
+ if (!remarkMath) {
4255
+ try {
4256
+ const remarkMathMod = await import("remark-math");
4257
+ remarkMath = remarkMathMod.default;
4258
+ } catch {
4259
+ }
4172
4260
  }
4261
+ return true;
4173
4262
  }
4174
4263
  function preprocessMarkdown(markdown, mathBlock) {
4175
4264
  markdown = markdown.replace(/\\\((.+?)\\\)/g, (_match, content) => `$${content}$`);
@@ -4193,9 +4282,11 @@ async function markdownToDelta(markdown, options = {}) {
4193
4282
  nodeHandlers = {}
4194
4283
  } = options;
4195
4284
  markdown = preprocessMarkdown(markdown, mathBlock);
4196
- await loadRemark();
4197
- if (!unified || !remarkParse) {
4198
- throw new Error("Failed to load remark");
4285
+ const loaded = await preloadRemark();
4286
+ if (!loaded || !unified || !remarkParse) {
4287
+ throw new Error(
4288
+ "remark is not installed. Install with: pnpm add unified remark-parse remark-gfm"
4289
+ );
4199
4290
  }
4200
4291
  let processor = unified().use(remarkParse);
4201
4292
  if (gfm && remarkGfm) {
@@ -4224,6 +4315,11 @@ function markdownToDeltaSync(markdown, options = {}) {
4224
4315
  } = options;
4225
4316
  markdown = preprocessMarkdown(markdown, mathBlock);
4226
4317
  if (!unified || !remarkParse) {
4318
+ if (typeof __require === "undefined") {
4319
+ throw new Error(
4320
+ "markdownToDeltaSync requires remark to be preloaded in this environment. `require()` is not available (likely browser ESM). Call `await preloadRemark()` once on application startup before using the sync API, or use the async `markdownToDelta()` instead."
4321
+ );
4322
+ }
4227
4323
  try {
4228
4324
  const unifiedMod = __require("unified");
4229
4325
  const remarkParseMod = __require("remark-parse");
@@ -4469,7 +4565,7 @@ function astToDelta(tree, customHandlers, mathBlock, mermaidBlock, plantumlBlock
4469
4565
  footnoteDefinitions.set(node.identifier ?? "", node);
4470
4566
  break;
4471
4567
  case "break":
4472
- context.pushNewline();
4568
+ context.pushEmbed({ softBreak: true });
4473
4569
  break;
4474
4570
  case "html": {
4475
4571
  const htmlContent = node.value ?? "";
@@ -4775,8 +4871,8 @@ function astToDelta(tree, customHandlers, mathBlock, mermaidBlock, plantumlBlock
4775
4871
  context.pushNewline();
4776
4872
  return;
4777
4873
  }
4778
- if (/^<br\s*\/?>$/i.test(html)) {
4779
- context.pushNewline();
4874
+ if (/^<br\b[^>]*\/?>$/i.test(html)) {
4875
+ context.pushEmbed({ softBreak: true });
4780
4876
  return;
4781
4877
  }
4782
4878
  }
@@ -4812,6 +4908,54 @@ function astToDelta(tree, customHandlers, mathBlock, mermaidBlock, plantumlBlock
4812
4908
  }
4813
4909
  return delta;
4814
4910
  }
4911
+
4912
+ // src/conversion/markdown/table-region.ts
4913
+ import { isInsert as isInsert4, isTextInsert as isTextInsert3 } from "@scrider/delta";
4914
+ function isTableNewlineOp(op) {
4915
+ if (!op || !isInsert4(op) || !isTextInsert3(op)) return false;
4916
+ if (!op.insert.includes("\n")) return false;
4917
+ return !!op.attributes && "table-row" in op.attributes;
4918
+ }
4919
+ function extractTableRegion(ops, hintOpIdx) {
4920
+ if (hintOpIdx < 0 || hintOpIdx >= ops.length) return null;
4921
+ let probeIdx = -1;
4922
+ for (let i = hintOpIdx; i < ops.length; i++) {
4923
+ const op = ops[i];
4924
+ if (!op || !isInsert4(op)) continue;
4925
+ if (isTextInsert3(op) && op.insert.includes("\n")) {
4926
+ probeIdx = i;
4927
+ break;
4928
+ }
4929
+ }
4930
+ if (probeIdx < 0) return null;
4931
+ if (!isTableNewlineOp(ops[probeIdx])) return null;
4932
+ let endOpIdx = probeIdx;
4933
+ for (let i = probeIdx + 1; i < ops.length; i++) {
4934
+ const op = ops[i];
4935
+ if (!op || !isInsert4(op)) break;
4936
+ if (isTextInsert3(op) && op.insert.includes("\n")) {
4937
+ if (isTableNewlineOp(op)) {
4938
+ endOpIdx = i;
4939
+ } else {
4940
+ break;
4941
+ }
4942
+ }
4943
+ }
4944
+ let startOpIdx = 0;
4945
+ for (let i = probeIdx - 1; i >= 0; i--) {
4946
+ const op = ops[i];
4947
+ if (!op || !isInsert4(op)) {
4948
+ startOpIdx = i + 1;
4949
+ break;
4950
+ }
4951
+ if (isTextInsert3(op) && op.insert.includes("\n") && !isTableNewlineOp(op)) {
4952
+ startOpIdx = i + 1;
4953
+ break;
4954
+ }
4955
+ }
4956
+ const regionOps = ops.slice(startOpIdx, endOpIdx + 1);
4957
+ return { startOpIdx, endOpIdx, ops: regionOps };
4958
+ }
4815
4959
  export {
4816
4960
  ALERT_TYPES,
4817
4961
  BOX_FLOAT_VALUES,
@@ -4845,6 +4989,7 @@ export {
4845
4989
  dividerFormat,
4846
4990
  escapeHtml,
4847
4991
  extractBoxOpAttributes,
4992
+ extractTableRegion,
4848
4993
  fontFormat,
4849
4994
  footnoteRefFormat,
4850
4995
  footnotesBlockHandler,
@@ -4859,6 +5004,7 @@ export {
4859
5004
  isAdapterAvailable,
4860
5005
  isElement,
4861
5006
  isRemarkAvailable,
5007
+ isTableNewlineOp,
4862
5008
  isTextNode,
4863
5009
  isValidColor,
4864
5010
  isValidHexColor,
@@ -4871,10 +5017,12 @@ export {
4871
5017
  markdownToDeltaSync,
4872
5018
  nodeAdapter,
4873
5019
  normalizeDelta,
5020
+ preloadRemark,
4874
5021
  sanitizeDelta,
4875
5022
  sizeFormat,
4876
5023
  slugify,
4877
5024
  slugifyWithDedup,
5025
+ softBreakFormat,
4878
5026
  strikeFormat,
4879
5027
  subscriptFormat,
4880
5028
  superscriptFormat,