@uniweb/content-writer 0.2.6 → 0.2.7
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/package.json +2 -2
- package/src/marks.js +6 -0
- package/src/normalize.js +10 -0
- package/tests/normalize.test.js +23 -0
- package/tests/roundtrip.test.js +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/content-writer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "ProseMirror document structure to Markdown converter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"vitest": "^4.1.7",
|
|
22
|
-
"@uniweb/content-reader": "1.1.
|
|
22
|
+
"@uniweb/content-reader": "1.1.14"
|
|
23
23
|
},
|
|
24
24
|
"directories": {
|
|
25
25
|
"test": "tests"
|
package/src/marks.js
CHANGED
|
@@ -287,6 +287,12 @@ function serializePlainNode(node) {
|
|
|
287
287
|
const latex = node.attrs?.latex || ''
|
|
288
288
|
return node.attrs?.display ? '$$' + latex + '$$' : '$' + latex + '$'
|
|
289
289
|
}
|
|
290
|
+
if (node.type === 'hardBreak') {
|
|
291
|
+
// Backslash rather than the two-trailing-spaces spelling: it survives
|
|
292
|
+
// editors, formatters and pre-commit hooks that strip trailing whitespace,
|
|
293
|
+
// and it is visible in a diff. Both spellings read back as a hard break.
|
|
294
|
+
return '\\\n'
|
|
295
|
+
}
|
|
290
296
|
if (node.type !== 'text') {
|
|
291
297
|
// No silent drops: an inline node we have no markdown form for is
|
|
292
298
|
// still omitted (we can't invent one) but reported loudly, the same
|
package/src/normalize.js
CHANGED
|
@@ -180,6 +180,16 @@ function mapEditorNode(node) {
|
|
|
180
180
|
return mapVideo(node)
|
|
181
181
|
case 'UniwebIcon':
|
|
182
182
|
return mapUniwebIcon(node)
|
|
183
|
+
case 'text':
|
|
184
|
+
// Legacy hard break: content-reader used to emit a hard break as a
|
|
185
|
+
// standalone "\n" text node. Self-heal it to the node form so old
|
|
186
|
+
// documents converge on the next write.
|
|
187
|
+
//
|
|
188
|
+
// Deliberately narrow — ONLY a text node whose text is exactly "\n".
|
|
189
|
+
// A newline INSIDE a larger text node is a SOFT break (an ordinary
|
|
190
|
+
// wrapped paragraph), which is a space and must stay one; mapping those
|
|
191
|
+
// would inject a visible break into every wrapped paragraph.
|
|
192
|
+
return node.text === '\n' ? { type: 'hardBreak' } : null
|
|
183
193
|
default:
|
|
184
194
|
return null
|
|
185
195
|
}
|
package/tests/normalize.test.js
CHANGED
|
@@ -195,4 +195,27 @@ describe('no-silent-drop guard: unmappable nodes are reported, not lost', () =>
|
|
|
195
195
|
expect(md).toBe('before after')
|
|
196
196
|
expect(warnings.some((m) => m.includes('FancyInlineWidget'))).toBe(true)
|
|
197
197
|
})
|
|
198
|
+
|
|
199
|
+
test('a legacy standalone "\\n" text node self-heals to a hard break', () => {
|
|
200
|
+
const md = proseMirrorToMarkdown(
|
|
201
|
+
doc(
|
|
202
|
+
para(
|
|
203
|
+
{ type: 'text', text: 'line one' },
|
|
204
|
+
{ type: 'text', text: '\n' },
|
|
205
|
+
{ type: 'text', text: 'line two' }
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
expect(md.trim()).toBe('line one\\\nline two')
|
|
210
|
+
expect(warnings).toEqual([]) // healed, not reported as unmapped
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('a newline INSIDE a text node is a soft break and must not be converted', () => {
|
|
214
|
+
// The narrow rule matters: this is an ordinary wrapped paragraph, which is
|
|
215
|
+
// a space. Converting it would inject a break into every wrapped paragraph.
|
|
216
|
+
const md = proseMirrorToMarkdown(
|
|
217
|
+
doc(para({ type: 'text', text: 'wrapped\nparagraph' }))
|
|
218
|
+
)
|
|
219
|
+
expect(md.trim()).toBe('wrapped\nparagraph')
|
|
220
|
+
})
|
|
198
221
|
})
|
package/tests/roundtrip.test.js
CHANGED
|
@@ -358,3 +358,34 @@ describe('Round-trip: Mixed Content', () => {
|
|
|
358
358
|
expect(reparsed).toEqual(parsed)
|
|
359
359
|
})
|
|
360
360
|
})
|
|
361
|
+
|
|
362
|
+
describe('Round-trip: Hard breaks', () => {
|
|
363
|
+
// A hard break used to become a "\n" text node, which re-parsed as a SOFT
|
|
364
|
+
// break — so the break was silently downgraded on every round trip. It is a
|
|
365
|
+
// `hardBreak` node now, and both markdown spellings converge on the
|
|
366
|
+
// backslash form.
|
|
367
|
+
testRoundTrip('line one\\\nline two', 'backslash hard break')
|
|
368
|
+
testRoundTrip('a\\\nb\\\nc', 'consecutive hard breaks')
|
|
369
|
+
testRoundTrip('**bold**\\\nplain', 'hard break after a mark')
|
|
370
|
+
|
|
371
|
+
test('two-space spelling normalizes to backslash and keeps the break', () => {
|
|
372
|
+
const parsed = markdownToProseMirror('line one \nline two')
|
|
373
|
+
expect(parsed.content[0].content).toEqual([
|
|
374
|
+
{ type: 'text', text: 'line one' },
|
|
375
|
+
{ type: 'hardBreak' },
|
|
376
|
+
{ type: 'text', text: 'line two' }
|
|
377
|
+
])
|
|
378
|
+
// One-time source normalization; the break itself survives.
|
|
379
|
+
const serialized = proseMirrorToMarkdown(parsed).trim()
|
|
380
|
+
expect(serialized).toBe('line one\\\nline two')
|
|
381
|
+
expect(markdownToProseMirror(serialized)).toEqual(parsed)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('a soft break stays soft — it is a space, not a break', () => {
|
|
385
|
+
const parsed = markdownToProseMirror('wrapped\nparagraph')
|
|
386
|
+
expect(parsed.content[0].content).toEqual([
|
|
387
|
+
{ type: 'text', text: 'wrapped\nparagraph' }
|
|
388
|
+
])
|
|
389
|
+
expect(proseMirrorToMarkdown(parsed).trim()).toBe('wrapped\nparagraph')
|
|
390
|
+
})
|
|
391
|
+
})
|