@uniweb/content-writer 0.3.0 → 0.3.1
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 +1 -1
- package/src/normalize.js +37 -16
- package/tests/roundtrip-source.test.js +19 -7
package/package.json
CHANGED
package/src/normalize.js
CHANGED
|
@@ -193,14 +193,8 @@ function mapEditorNode(node) {
|
|
|
193
193
|
case 'Icon':
|
|
194
194
|
return mapUniwebIcon(node)
|
|
195
195
|
case 'WarningBlock':
|
|
196
|
-
case 'details':
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
type: 'inset_block',
|
|
200
|
-
attrs,
|
|
201
|
-
content: node.type === 'details' ? detailsBody(node) : asBlocks(node.content),
|
|
202
|
-
}
|
|
203
|
-
}
|
|
196
|
+
case 'details':
|
|
197
|
+
return EDITOR_CONTAINERS[node.type](node)
|
|
204
198
|
case 'emoji':
|
|
205
199
|
// The node carries the character alongside a name; the character is the
|
|
206
200
|
// whole markdown form. Guarded rather than assumed to be populated.
|
|
@@ -256,8 +250,25 @@ const NAMED_INLINE_STYLES = new Set(['accent', 'highlight', 'callout', 'muted'])
|
|
|
256
250
|
* missing entry here is a visible gap rather than a silent assumption.
|
|
257
251
|
*/
|
|
258
252
|
const EDITOR_CONTAINERS = {
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
// A callout's SEVERITY is the concept, not a parameter on one — a warning and
|
|
254
|
+
// a note are different kinds of thing, so they are different tags. This is
|
|
255
|
+
// where the editor's `type` attribute goes.
|
|
256
|
+
WarningBlock: node => ({
|
|
257
|
+
type: 'concept_block',
|
|
258
|
+
attrs: { tag: node.attrs?.type || 'info' },
|
|
259
|
+
content: asBlocks(node.content),
|
|
260
|
+
}),
|
|
261
|
+
// The summary becomes the leading HEADING rather than a paragraph, which is
|
|
262
|
+
// what makes the mapping lossless in both directions: a concept block's items
|
|
263
|
+
// come from its headings, so the summary recovers as that item's title and
|
|
264
|
+
// the body as its paragraphs. Flattening it to a paragraph — what this did
|
|
265
|
+
// while the target was an inset — made the two indistinguishable on the way
|
|
266
|
+
// back.
|
|
267
|
+
details: node => ({
|
|
268
|
+
type: 'concept_block',
|
|
269
|
+
attrs: { tag: 'details' },
|
|
270
|
+
content: detailsBody(node),
|
|
271
|
+
}),
|
|
261
272
|
}
|
|
262
273
|
|
|
263
274
|
/** Inline node types, for deciding whether a container's body needs wrapping. */
|
|
@@ -289,11 +300,18 @@ function asBlocks(content) {
|
|
|
289
300
|
}
|
|
290
301
|
|
|
291
302
|
/**
|
|
292
|
-
* Flatten the editor's `details` › `detailsSummary` + `detailsContent` into
|
|
293
|
-
*
|
|
303
|
+
* Flatten the editor's `details` › `detailsSummary` + `detailsContent` into a
|
|
304
|
+
* concept block's body, summary first — as a HEADING.
|
|
294
305
|
*
|
|
295
|
-
* The
|
|
296
|
-
*
|
|
306
|
+
* The heading is what makes the mapping lossless. A concept block's items come
|
|
307
|
+
* from its headings, so a summary written as `# …` recovers as that item's
|
|
308
|
+
* title and the body as its paragraphs; written as a paragraph (which is what
|
|
309
|
+
* this did while the target was an inset) the two become indistinguishable on
|
|
310
|
+
* the way back, and a disclosure loses which half was its label.
|
|
311
|
+
*
|
|
312
|
+
* The summary stays inline content rather than moving to an attribute for the
|
|
313
|
+
* original reason: `detailsSummary` can carry marks, and `summary="…"` would
|
|
314
|
+
* flatten emphasis and links.
|
|
297
315
|
*
|
|
298
316
|
* @param {Object} node
|
|
299
317
|
* @returns {Array}
|
|
@@ -301,8 +319,11 @@ function asBlocks(content) {
|
|
|
301
319
|
function detailsBody(node) {
|
|
302
320
|
const body = []
|
|
303
321
|
for (const child of node.content || []) {
|
|
304
|
-
if (child.type === 'detailsSummary')
|
|
305
|
-
|
|
322
|
+
if (child.type === 'detailsSummary') {
|
|
323
|
+
if (child.content?.length) {
|
|
324
|
+
body.push({ type: 'heading', attrs: { level: 1 }, content: child.content })
|
|
325
|
+
}
|
|
326
|
+
} else if (child.type === 'detailsContent') body.push(...(child.content || []))
|
|
306
327
|
else body.push(child)
|
|
307
328
|
}
|
|
308
329
|
return body
|
|
@@ -263,22 +263,34 @@ describe('containers — the block form of an inset', () => {
|
|
|
263
263
|
expect(rt(source)).toBe(source)
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
-
test('the editor
|
|
266
|
+
test('the editor containers normalize onto CONCEPT blocks, not insets', () => {
|
|
267
|
+
// Changed 2026-07-31. These used to become `@Alert` and `@Details` — an
|
|
268
|
+
// inset naming the component that renders it, which is a rendering
|
|
269
|
+
// decision living in content. A concept block names what the content IS
|
|
270
|
+
// and leaves rendering to the foundation.
|
|
267
271
|
const text = t => ({ type: 'text', text: t })
|
|
268
272
|
const doc = node => proseMirrorToMarkdown({ type: 'doc', content: [node] }).trim()
|
|
269
273
|
|
|
270
|
-
//
|
|
271
|
-
//
|
|
274
|
+
// The editor's `type` becomes the TAG: a warning and a note are different
|
|
275
|
+
// kinds of thing, not one kind with a parameter. `info` is still the
|
|
276
|
+
// default, and still not `warning` — a name-derived mapping drops that.
|
|
272
277
|
expect(doc({ type: 'WarningBlock', attrs: {}, content: [text('Careful.')] }))
|
|
273
|
-
.toBe('
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
.toBe('```md:info\nCareful.\n```')
|
|
279
|
+
expect(doc({ type: 'WarningBlock', attrs: { type: 'warning' }, content: [text('Careful.')] }))
|
|
280
|
+
.toBe('```md:warning\nCareful.\n```')
|
|
281
|
+
|
|
282
|
+
// The summary becomes the leading HEADING, which is what makes this
|
|
283
|
+
// lossless: a concept block's items come from its headings, so the summary
|
|
284
|
+
// recovers as the item's title and the body as its paragraphs. As a
|
|
285
|
+
// paragraph — what this produced while the target was an inset — the two
|
|
286
|
+
// were indistinguishable coming back.
|
|
276
287
|
expect(doc({
|
|
277
288
|
type: 'details',
|
|
278
289
|
content: [
|
|
279
290
|
{ type: 'detailsSummary', content: [text('Breaking '), { type: 'text', text: 'changes', marks: [{ type: 'bold' }] }] },
|
|
280
291
|
{ type: 'detailsContent', content: [{ type: 'paragraph', content: [text('Body.')] }] },
|
|
281
292
|
],
|
|
282
|
-
})).toBe('
|
|
293
|
+
})).toBe('```md:details\n# Breaking **changes**\n\nBody.\n```')
|
|
283
294
|
})
|
|
295
|
+
|
|
284
296
|
})
|