@markup-carve/carve-grammars 0.1.7 → 0.1.8

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 CHANGED
@@ -259,6 +259,16 @@ available as the `editor` CSS part (`carve-editor::part(editor)`). Tiptap stays
259
259
  an implementation detail of the element, although its peer packages must be
260
260
  installed with `carve-grammars`.
261
261
 
262
+ Document front matter appears as a collapsed **Document metadata** card. Expand
263
+ it to edit the common `title`, `lang`, `author`, and `description` fields, or use
264
+ the raw YAML/TOML field for custom metadata. Both paths update the document
265
+ through the editor, so they participate in undo/redo and preserve unknown keys.
266
+ Unsupported-source atoms use the same compact/editable pattern for their exact
267
+ Carve payload. Inline footnotes, cross-references, and citations open target
268
+ pickers, while abbreviation and link-reference definitions use collapsible
269
+ forms. Import `@markup-carve/carve-grammars/tiptap/editor.css` when mounting
270
+ `CarveKit` directly to receive the associated editor chrome and table styling.
271
+
262
272
  ## Syntax highlighting
263
273
 
264
274
  Render Carve source as highlighted HTML on the web. Both grammars cover the full
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markup-carve/carve-grammars",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Grammars for the Carve markup language: Tiptap editor kit + serializer, plus Prism, highlight.js and TextMate syntax-highlighting grammars",
5
5
  "type": "module",
6
6
  "main": "tiptap/index.js",
@@ -32,6 +32,73 @@ export class CarveEditorElement extends HTMLElementBase {
32
32
  [part="editor"] { min-height: 8rem; }
33
33
  .ProseMirror { min-height: inherit; outline: none; }
34
34
  .ProseMirror:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
35
+ .carve-frontmatter-card { border: 1px solid #d5d9df; border-radius: 4px; margin-block-end: 1rem; }
36
+ .carve-frontmatter-summary { width: 100%; border: 0; background: #f4f5f7; color: inherit; cursor: pointer; font: inherit; font-weight: 600; padding: .65rem .8rem; text-align: start; }
37
+ .carve-frontmatter-summary::before { content: '▸'; display: inline-block; margin-inline-end: .5rem; }
38
+ .carve-frontmatter-summary[aria-expanded="true"]::before { transform: rotate(90deg); }
39
+ .carve-frontmatter-body { padding: .8rem; }
40
+ .carve-frontmatter-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: .75rem; }
41
+ .carve-frontmatter-card label { display: grid; gap: .25rem; font-size: .875rem; font-weight: 600; }
42
+ .carve-frontmatter-card input, .carve-frontmatter-card textarea { box-sizing: border-box; width: 100%; border: 1px solid #b9c0ca; border-radius: 3px; font: inherit; padding: .5rem; }
43
+ .carve-frontmatter-raw { margin-block-start: .75rem; }
44
+ .carve-frontmatter-raw textarea { min-height: 7rem; font-family: monospace; font-weight: 400; }
45
+ .ProseMirror table { width: 100%; border-collapse: collapse; outline: 1px solid #d5d9df; }
46
+ .ProseMirror th, .ProseMirror td { border: 1px solid #d5d9df; padding: .5rem; vertical-align: top; }
47
+ .ProseMirror th { background: #f4f5f7; font-weight: 700; }
48
+ .ProseMirror a { color: #1769c2; text-underline-offset: .14em; }
49
+ .ProseMirror blockquote { margin: 0 0 1rem; padding-inline-start: 1rem; border-inline-start: 3px solid #c8ced7; color: #5a626d; }
50
+ .ProseMirror blockquote:focus-within { border-inline-start-color: #1769c2; }
51
+ .ProseMirror blockquote > :first-child { margin-block-start: 0; }
52
+ .ProseMirror blockquote > :last-child { margin-block-end: 0; }
53
+ .ProseMirror ul[data-type="taskList"] { list-style: none; padding-inline-start: 0; }
54
+ .ProseMirror ul[data-type="taskList"] ul[data-type="taskList"] { padding-inline-start: 1.5rem; }
55
+ .ProseMirror ul[data-type="taskList"] > li[data-checked] { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; column-gap: .5rem; }
56
+ .ProseMirror ul[data-type="taskList"] > li[data-checked] > label { margin-block-start: .2em; line-height: inherit; }
57
+ .ProseMirror ul[data-type="taskList"] > li[data-checked] > div > :first-child { margin-block-start: 0; }
58
+ .ProseMirror ul[data-type="taskList"] > li[data-checked] > div > :last-child { margin-block-end: 0; }
59
+ .ProseMirror pre[data-carve-comment] { margin-block: .75rem; border: 1px dashed #7a828c; border-radius: 4px; background: #f4f5f7; color: #5a626d; padding: .55rem .75rem .65rem; white-space: pre-wrap; }
60
+ .ProseMirror pre[data-carve-comment]::before { content: "Comment"; display: block; margin-block-end: .35rem; color: #0e4459; font: 700 .7rem/1.2 system-ui, sans-serif; letter-spacing: .08em; text-transform: uppercase; }
61
+ .ProseMirror span[data-carve-comment-inline] { border: 1px dashed #7a828c; border-radius: 4px; background: #e6f0f4; color: #5a626d; padding: .05em .3em; }
62
+ .ProseMirror span[data-carve-comment-inline]::before { content: "Comment: "; color: #0e4459; font-size: .78em; font-weight: 700; }
63
+ .ProseMirror [data-carve-extension], .ProseMirror [data-carve-empty-mark-node] { border-radius: 3px; background: #f4f5f7; padding: .05em .3em; }
64
+ .ProseMirror [data-carve-extension]::before { content: ':' attr(data-carve-extension) '['; color: #0e4459; font: 600 .78em/1 monospace; margin-inline-end: .2em; }
65
+ .ProseMirror [data-carve-extension]::after { content: ']'; color: #0e4459; font: 600 .78em/1 monospace; margin-inline-start: .2em; }
66
+ .ProseMirror [data-carve-empty-mark-node]::before { content: 'Empty ' attr(data-carve-empty-mark); color: #5a626d; font: 600 .78em/1 system-ui, sans-serif; }
67
+ .ProseMirror [data-carve-citation-definition], .ProseMirror .carve-footnote-definition, .ProseMirror figcaption[data-carve-caption] { border-inline-start: 3px solid #c8ced7; padding-inline-start: .75rem; }
68
+ .ProseMirror [data-carve-citation-definition]::before, .ProseMirror .carve-footnote-definition::before, .ProseMirror figcaption[data-carve-caption]::before { display: block; color: #5a626d; font: 700 .68rem/1.3 system-ui, sans-serif; letter-spacing: .06em; text-transform: uppercase; }
69
+ .ProseMirror [data-carve-citation-definition]::before { content: 'Citation · ' attr(key); }
70
+ .ProseMirror .carve-footnote-definition::before { content: 'Footnote · ' attr(data-footnote-label); }
71
+ .ProseMirror figcaption[data-carve-caption]::before { content: 'Caption'; }
72
+ .ProseMirror .critic-comment { background: #fff6dc; text-decoration: underline dotted #9a6700; text-underline-offset: .2em; }
73
+ .carve-code-block { position: relative; margin-block: .75rem; border: 1px solid #d5d9df; border-radius: 4px; overflow: hidden; }
74
+ .carve-code-block-chrome { display: flex; align-items: center; justify-content: flex-end; padding: .375rem .5rem; border-block-start: 1px solid #d5d9df; background: white; }
75
+ .carve-code-block-chrome > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
76
+ .carve-code-lang { max-width: 12rem; border: 1px solid #1769c2; border-radius: 3px; background: #1769c2; color: white; font: inherit; font-weight: 600; padding: .3rem .55rem; }
77
+ .carve-code-block > pre { margin: 0; border: 0; border-radius: 0; padding: var(--carve-code-padding, .75rem); }
78
+ .carve-inline-control, .carve-raw-atom-inline { position: relative; display: inline-flex; }
79
+ .carve-inline-control-trigger, .carve-raw-atom-summary { border: 1px solid #c8ced7; border-radius: 999px; background: #f4f5f7; color: inherit; cursor: pointer; font: inherit; padding: .1em .45em; }
80
+ .carve-inline-control-editor, .carve-raw-atom-inline .carve-raw-atom-editor { position: absolute; z-index: 20; inset-block-start: 100%; inset-inline-start: 0; display: grid; grid-template-columns: 1fr auto; gap: .4rem; min-width: min(22rem, 80vw); padding: .5rem; border: 1px solid #c8ced7; background: white; }
81
+ .carve-inline-control-editor[hidden], .carve-raw-atom-editor[hidden] { display: none; }
82
+ .carve-inline-control-editor > input, .carve-inline-choices { grid-column: 1 / -1; }
83
+ .carve-inline-control-editor > .carve-control-primary { justify-self: end; }
84
+ .carve-inline-fields-editor > label { grid-column: 1 / -1; display: grid; gap: .2rem; color: #5a626d; font-size: .8rem; }
85
+ .carve-inline-fields-editor textarea { min-height: 4.5rem; resize: vertical; }
86
+ .carve-inline-fields-editor input, .carve-inline-fields-editor textarea { width: 100%; min-width: 0; font: inherit; }
87
+ .carve-inline-fields-editor input[type="checkbox"] { width: auto; justify-self: start; }
88
+ .carve-inline-field-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; gap: .4rem; }
89
+ .carve-embed-editor { display: grid; grid-template-columns: 1fr auto auto; gap: .4rem; padding: .6rem; border-block-start: 1px solid #c8ced7; background: white; }
90
+ .carve-embed-editor[hidden] { display: none; }
91
+ .carve-embed-editor label { grid-column: 1 / -1; display: grid; gap: .25rem; }
92
+ .carve-embed-editor input { width: 100%; min-width: 0; font: inherit; }
93
+ .carve-inline-choices { display: grid; max-height: 11rem; overflow-y: auto; border: 1px solid #d5d9df; border-radius: 3px; padding: .2rem; }
94
+ .carve-inline-choices[hidden] { display: none; }
95
+ .carve-inline-choice { width: 100%; border: 0; background: transparent; color: inherit; padding: .4rem .5rem; text-align: start; }
96
+ .carve-inline-choice:hover, .carve-inline-choice:focus-visible, .carve-inline-choice[aria-selected="true"] { background: #e6f0f4; color: #0e4459; }
97
+ .carve-definition-card, .carve-raw-atom:not(.carve-raw-atom-inline) { border: 1px solid #d5d9df; border-radius: 4px; margin-block: .75rem; }
98
+ .carve-definition-summary, .carve-raw-atom:not(.carve-raw-atom-inline) > .carve-raw-atom-summary { display: block; width: 100%; border: 0; border-radius: 0; text-align: start; font-weight: 600; padding: .6rem .75rem; }
99
+ .carve-definition-body, .carve-raw-atom:not(.carve-raw-atom-inline) > .carve-raw-atom-editor { padding: .75rem; }
100
+ .carve-definition-body label { display: grid; gap: .25rem; margin-block-end: .6rem; }
101
+ .carve-definition-body input, .carve-definition-body textarea, .carve-raw-atom-editor textarea { box-sizing: border-box; width: 100%; padding: .5rem; }
35
102
  </style>
36
103
  <div part="editor"></div>
37
104
  `;
@@ -62,6 +129,9 @@ export class CarveEditorElement extends HTMLElementBase {
62
129
  attributeChangedCallback(name) {
63
130
  if (name === 'readonly' && this._editor) {
64
131
  this._editor.setEditable(!this.hasAttribute('readonly'));
132
+ for (const control of this.shadowRoot.querySelectorAll('.carve-frontmatter-card input, .carve-frontmatter-card textarea, [data-carve-edit-control]')) {
133
+ control.disabled = this.hasAttribute('readonly');
134
+ }
65
135
  }
66
136
  }
67
137
 
@@ -1,4 +1,5 @@
1
1
  import { Extension } from '@tiptap/core';
2
+ import { Plugin } from '@tiptap/pm/state';
2
3
  import StarterKit from '@tiptap/starter-kit';
3
4
  import CodeBlock from '@tiptap/extension-code-block';
4
5
  import Highlight from '@tiptap/extension-highlight';
@@ -67,6 +68,7 @@ import { CarveMention, CarveTag } from './extensions/carve-mention.js';
67
68
  // shown even if it is not in this list.
68
69
  const CODE_LANGS = [
69
70
  { value: '', label: 'Plain text' },
71
+ { value: 'carve', label: 'Carve' },
70
72
  { value: 'php', label: 'PHP' },
71
73
  { value: 'javascript', label: 'JavaScript' },
72
74
  { value: 'typescript', label: 'TypeScript' },
@@ -147,12 +149,68 @@ function authoredClasses(element) {
147
149
  return kept.length ? kept.join(' ') : null;
148
150
  }
149
151
 
152
+ const LEGACY_INLINE_CONTENT_NODES = new Set([
153
+ 'carveCommentInline',
154
+ 'carveLiteral',
155
+ 'carveRawInline',
156
+ ]);
157
+
158
+ // These nodes used to store their editable payload in attrs.content. Keep old
159
+ // persisted ProseMirror JSON usable by promoting that attribute to child text
160
+ // as soon as it enters an editor. Applying replacements from right to left
161
+ // keeps every collected position valid as leaf nodes grow children.
162
+ function legacyInlineContentTransaction(state) {
163
+ const replacements = [];
164
+ state.doc.descendants((node, pos) => {
165
+ const legacyContent = node.attrs?.content;
166
+ if (LEGACY_INLINE_CONTENT_NODES.has(node.type.name)
167
+ && node.childCount === 0
168
+ && typeof legacyContent === 'string'
169
+ && legacyContent.length > 0) {
170
+ replacements.push({ node, pos, legacyContent });
171
+ }
172
+ });
173
+ if (replacements.length === 0) return null;
174
+
175
+ const transaction = state.tr;
176
+ for (const { node, pos, legacyContent } of replacements.reverse()) {
177
+ transaction.replaceWith(
178
+ pos,
179
+ pos + node.nodeSize,
180
+ node.type.create(
181
+ { ...node.attrs, content: null },
182
+ state.schema.text(legacyContent),
183
+ node.marks,
184
+ ),
185
+ );
186
+ }
187
+ transaction.setMeta('addToHistory', false);
188
+ transaction.setMeta('carveLegacyInlineContentMigration', true);
189
+ return transaction;
190
+ }
191
+
150
192
  export const CarveKit = Extension.create({
151
193
  name: 'carveKit',
152
194
 
153
195
  addExtensions() {
154
196
  const extensions = [];
155
197
 
198
+ extensions.push(Extension.create({
199
+ name: 'carveLegacyInlineContentMigration',
200
+ onCreate() {
201
+ const transaction = legacyInlineContentTransaction(this.editor.state);
202
+ if (transaction) this.editor.view.dispatch(transaction);
203
+ },
204
+ addProseMirrorPlugins() {
205
+ return [new Plugin({
206
+ appendTransaction(transactions, _oldState, newState) {
207
+ if (transactions.some(transaction => transaction.getMeta('carveLegacyInlineContentMigration'))) return null;
208
+ return legacyInlineContentTransaction(newState);
209
+ },
210
+ })];
211
+ },
212
+ }));
213
+
156
214
  // Attributes on ordinary block nodes are part of the Carve document,
157
215
  // but the stock Tiptap schema does not declare them. ProseMirror drops
158
216
  // undeclared fields while mounting JSON, so retain the common Carve
@@ -262,9 +320,11 @@ export const CarveKit = Extension.create({
262
320
  };
263
321
  },
264
322
 
265
- // Floating language picker: a <select> in the corner of every
266
- // code block that shows the current language and edits it in
267
- // place (the toolbar can't show a per-block value). Disable with
323
+ // Language toolbar below every code block. Keeping the control
324
+ // outside <pre>/<code> means it never consumes the first line's
325
+ // horizontal space or becomes part of the editable content,
326
+ // while placing it after the source keeps metadata secondary.
327
+ // Disable with
268
328
  // CarveKit.configure({ codeBlock: { languagePicker: false } }).
269
329
  addNodeView() {
270
330
  if (this.options.languagePicker === false) {
@@ -272,6 +332,8 @@ export const CarveKit = Extension.create({
272
332
  }
273
333
  return ({ node, editor, getPos }) => {
274
334
  let current = node;
335
+ const dom = document.createElement('div');
336
+ dom.className = 'carve-code-block';
275
337
  const pre = document.createElement('pre');
276
338
  if (node.attrs.carveLanguageRaw) {
277
339
  pre.setAttribute('data-language-raw', node.attrs.carveLanguageRaw);
@@ -281,6 +343,11 @@ export const CarveKit = Extension.create({
281
343
  select.className = 'carve-code-lang';
282
344
  select.contentEditable = 'false';
283
345
  select.setAttribute('aria-label', 'Code language');
346
+ const chrome = document.createElement('div');
347
+ chrome.className = 'carve-code-block-chrome';
348
+ chrome.contentEditable = 'false';
349
+ const label = document.createElement('span');
350
+ label.textContent = 'Language';
284
351
  const fill = (lang) => {
285
352
  select.innerHTML = '';
286
353
  const opts = CODE_LANGS.slice();
@@ -318,11 +385,12 @@ export const CarveKit = Extension.create({
318
385
  code.className = lang ? `language-${lang}` : '';
319
386
  };
320
387
  applyLangClass(node.attrs.language || '');
321
- pre.appendChild(select);
322
388
  pre.appendChild(code);
389
+ chrome.append(label, select);
390
+ dom.append(pre, chrome);
323
391
 
324
392
  return {
325
- dom: pre,
393
+ dom,
326
394
  contentDOM: code,
327
395
  update: (updated) => {
328
396
  if (updated.type !== current.type) {
@@ -336,8 +404,8 @@ export const CarveKit = Extension.create({
336
404
  return true;
337
405
  },
338
406
  // The <select> is chrome, not editable content.
339
- ignoreMutation: (m) => select.contains(m.target),
340
- stopEvent: (e) => select.contains(e.target),
407
+ ignoreMutation: (m) => chrome.contains(m.target),
408
+ stopEvent: (e) => chrome.contains(e.target),
341
409
  };
342
410
  };
343
411
  },
@@ -396,6 +464,25 @@ export const CarveKit = Extension.create({
396
464
  carveBareMarker: { default: null },
397
465
  };
398
466
  },
467
+ addCommands() {
468
+ return {
469
+ ...this.parent?.(),
470
+ // A list CREATED in Tiptap has no authored marker to
471
+ // preserve. Prefer Carve's constant-width automatic
472
+ // marker without changing the schema default: keeping
473
+ // that nullable preserves older persisted PM JSON.
474
+ toggleOrderedList: () => ({ chain, editor }) => {
475
+ const wasActive = editor.isActive(this.name);
476
+ const command = chain().toggleList(
477
+ this.name, this.options.itemTypeName, this.options.keepMarks,
478
+ );
479
+ if (wasActive) return command.run();
480
+ return command.updateAttributes(this.name, {
481
+ carveBareMarker: true, carveDelim: '.',
482
+ }).run();
483
+ },
484
+ };
485
+ },
399
486
  });
400
487
  extensions.push(CustomOrderedList.configure(this.options.orderedList ?? {}));
401
488
  }
@@ -532,9 +619,26 @@ export const CarveKit = Extension.create({
532
619
  resizable: true,
533
620
  ...this.options.table,
534
621
  }));
622
+ const tableAlign = {
623
+ default: null,
624
+ parseHTML: element => !element.hasAttribute('data-carve-inherited-align')
625
+ && ['left', 'center', 'right'].includes(element.style.textAlign)
626
+ ? element.style.textAlign : null,
627
+ renderHTML: attrs => ['left', 'center', 'right'].includes(attrs.textAlign)
628
+ ? { style: `text-align: ${attrs.textAlign}` } : {},
629
+ };
630
+ const inheritedTableAlign = {
631
+ default: null,
632
+ parseHTML: () => null,
633
+ renderHTML: attrs => !attrs.textAlign && ['left', 'center', 'right'].includes(attrs.carveInheritedTextAlign)
634
+ ? {
635
+ style: `text-align: ${attrs.carveInheritedTextAlign}`,
636
+ 'data-carve-inherited-align': attrs.carveInheritedTextAlign,
637
+ } : {},
638
+ };
535
639
  const tableAttrs = {
536
640
  id: { default: null }, class: { default: null }, carveKeyValues: { default: null },
537
- ...attributeOrderSlot(), textAlign: { default: null },
641
+ ...attributeOrderSlot(), textAlign: tableAlign, carveInheritedTextAlign: inheritedTableAlign,
538
642
  };
539
643
  const CustomTableRow = TableRow.extend({ addAttributes() { return { ...this.parent?.(), ...tableAttrs }; } });
540
644
  const CustomTableCell = TableCell.extend({ addAttributes() { return { ...this.parent?.(), ...tableAttrs }; } });
@@ -282,7 +282,7 @@ function unsupported(type, node, ctx) {
282
282
  if (ctx?.unsupported === 'preserve') {
283
283
  const carveSource = sourceFor(node, ctx);
284
284
  if (carveSource) {
285
- record(ctx, 'preserved', type, 'kept as exact source in a carveUnsupported atom; not editable');
285
+ record(ctx, 'preserved', type, 'kept as exact source in an editable carveUnsupported atom');
286
286
 
287
287
  // The TYPE the atom stands in for, not only its source. Without it
288
288
  // a caller can see that something was preserved and never learn
@@ -493,7 +493,10 @@ function convertBlock(node, ctx) {
493
493
  // Dropping it rewrote every alpha and roman list as `1.`.
494
494
  if (node.olType) listNode.attrs.carveOlType = node.olType;
495
495
  if (node.delim) listNode.attrs.carveDelim = node.delim;
496
- if (node.bareMarker) listNode.attrs.carveBareMarker = true;
496
+ // Spell this out in both directions. The schema default is
497
+ // `true` for newly authored Tiptap lists, while an imported
498
+ // explicit `1.` list must override that default with `false`.
499
+ listNode.attrs.carveBareMarker = node.bareMarker === true;
497
500
  }
498
501
  return listNode;
499
502
  }
@@ -774,6 +777,11 @@ function convertTable(node, ctx) {
774
777
 
775
778
  function convertTableCore(node, ctx) {
776
779
  let previousGrid = [];
780
+ const inheritedColumnAligns = (node.columns || []).map(column => column?.align || null);
781
+ const headingRow = (node.rows || []).find(row => (row.cells || []).some(cell => cell.header));
782
+ (headingRow?.cells || []).forEach((cell, col) => {
783
+ if (cell.header && cell.align) inheritedColumnAligns[col] = cell.align;
784
+ });
777
785
  const rows = (node.rows || []).map((row) => {
778
786
  const cells = [];
779
787
  const grid = [];
@@ -805,6 +813,7 @@ function convertTableCore(node, ctx) {
805
813
  }
806
814
  const attrs = { ...(convertAttrs(cell.attrs) || {}) };
807
815
  if (cell.align) attrs.textAlign = cell.align;
816
+ else if (inheritedColumnAligns[col]) attrs.carveInheritedTextAlign = inheritedColumnAligns[col];
808
817
  const converted = {
809
818
  type: cell.header ? 'tableHeader' : 'tableCell',
810
819
  attrs,
@@ -840,7 +849,7 @@ function convertInline(nodes, ctx) {
840
849
  const carveSource = sourceFor(node, ctx);
841
850
  if (carveSource) {
842
851
  record(ctx, 'preserved', error.nodeType || node.type,
843
- 'kept as exact source in a carveUnsupportedInline atom; not editable');
852
+ 'kept as exact source in an editable carveUnsupportedInline atom');
844
853
  out.push(unsupportedInlineSource(carveSource, error.nodeType || node.type));
845
854
  continue;
846
855
  }
@@ -1097,7 +1106,8 @@ function convertInlineNode(node, marks, ctx) {
1097
1106
  case 'literal_inline':
1098
1107
  return [{
1099
1108
  type: 'carveLiteral',
1100
- attrs: { content: node.content || '', ...(convertAttrs(node.attrs) || {}) },
1109
+ ...(convertAttrs(node.attrs) ? { attrs: convertAttrs(node.attrs) } : {}),
1110
+ content: node.content ? [{ type: 'text', text: node.content }] : [],
1101
1111
  ...(marks.length ? { marks } : {}),
1102
1112
  }];
1103
1113
 
@@ -1117,9 +1127,9 @@ function convertInlineNode(node, marks, ctx) {
1117
1127
  type: 'carveRawInline',
1118
1128
  attrs: {
1119
1129
  format: node.format || '',
1120
- content: node.content || '',
1121
1130
  ...(convertAttrs(node.attrs) || {}),
1122
1131
  },
1132
+ content: node.content ? [{ type: 'text', text: node.content }] : [],
1123
1133
  ...(marks.length ? { marks } : {}),
1124
1134
  }];
1125
1135
 
@@ -1165,7 +1175,8 @@ function convertInlineNode(node, marks, ctx) {
1165
1175
  case 'comment':
1166
1176
  return [{
1167
1177
  type: 'carveCommentInline',
1168
- attrs: { content: node.content || '', delimited: Boolean(node.delimited) },
1178
+ attrs: { delimited: Boolean(node.delimited) },
1179
+ content: node.content ? [{ type: 'text', text: node.content }] : [],
1169
1180
  }];
1170
1181
 
1171
1182
  case 'abbreviation':