@uniweb/content-writer 0.3.2 → 0.3.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/content-writer",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "ProseMirror document structure to Markdown converter",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "vitest": "^4.1.7",
23
- "@uniweb/content-reader": "1.2.1"
23
+ "@uniweb/content-reader": "1.2.2"
24
24
  },
25
25
  "directories": {
26
26
  "test": "tests"
package/src/normalize.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * link/button/code/span`), and
10
10
  * - the **editor (TipTap) dialect** (`ImageBlock`, `Video`,
11
11
  * `UniwebIcon`/`Icon`, `DividerBlock`, `card-group`, `FormBlock`,
12
- * `WarningBlock`, `details`, colour/highlight marks …).
12
+ * colour/highlight marks …).
13
13
  *
14
14
  * A ProseMirror document handed to content-writer may be in either dialect,
15
15
  * or a mix (file-origin content is framework-dialect; editor-authored
@@ -46,8 +46,15 @@
46
46
  * - `FormBlock` — the editor node carries `activeSchemaId` + filled
47
47
  * `data`; the framework `yaml:form` block carries a form *definition*.
48
48
  * Different shapes; mapping needs a decision, not a guess.
49
- * - `card-group`/`document-group`, `details`, `WarningBlock` — open
50
- * mapping decisions (items vs insets; 🟥 gaps).
49
+ * - `card-group`/`document-group` — open mapping decisions (🟥 gaps).
50
+ *
51
+ * The editor's pre-concept containers — `WarningBlock`, `details`,
52
+ * `detailsSummary`, `detailsContent` — used to be mapped here. The editor
53
+ * now registers `concept_block` and its rows directly, so it never emits
54
+ * them and the conversion was dead code; it was deleted when the
55
+ * regenerated fixture in `_contracts/editor-vocabulary-diff.test.js`
56
+ * stopped listing them. That fixture is the signal, and it is the only one
57
+ * there is — no message announces it.
51
58
  * - colour/highlight/strike/underline marks — need the named-inline-
52
59
  * style convention decision.
53
60
  *
@@ -192,9 +199,6 @@ function mapEditorNode(node) {
192
199
  case 'UniwebIcon':
193
200
  case 'Icon':
194
201
  return mapUniwebIcon(node)
195
- case 'WarningBlock':
196
- case 'details':
197
- return EDITOR_CONTAINERS[node.type](node)
198
202
  case 'emoji':
199
203
  // The node carries the character alongside a name; the character is the
200
204
  // whole markdown form. Guarded rather than assumed to be populated.
@@ -240,95 +244,6 @@ function mapEditorNode(node) {
240
244
  */
241
245
  const NAMED_INLINE_STYLES = new Set(['accent', 'highlight', 'callout', 'muted'])
242
246
 
243
- /**
244
- * Editor container nodes → the framework's `inset_block`, as an explicit table.
245
- *
246
- * Deliberately not derived from the node name. The editor's node is
247
- * `WarningBlock` while the component is `Alert`, and its `type` defaults to
248
- * `info` rather than `warning` — a name-derived mapping drops exactly that row,
249
- * which is how `mapUniwebIcon` came to lose every icon the editor produces. A
250
- * missing entry here is a visible gap rather than a silent assumption.
251
- */
252
- const EDITOR_CONTAINERS = {
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
- }),
272
- }
273
-
274
- /** Inline node types, for deciding whether a container's body needs wrapping. */
275
- const INLINE_TYPES = new Set([
276
- 'text',
277
- 'hardBreak',
278
- 'image',
279
- 'math_inline',
280
- 'inset_ref',
281
- 'emoji',
282
- 'UniwebIcon',
283
- ])
284
-
285
- /**
286
- * A container body as block content.
287
- *
288
- * `WarningBlock` holds `text*` today and `block+` after the editor's widening,
289
- * so both shapes arrive here. Inline content is wrapped in one paragraph;
290
- * block content passes through.
291
- *
292
- * @param {Array} content
293
- * @returns {Array}
294
- */
295
- function asBlocks(content) {
296
- if (!content?.length) return []
297
- return content.every(node => INLINE_TYPES.has(node.type))
298
- ? [{ type: 'paragraph', content }]
299
- : content
300
- }
301
-
302
- /**
303
- * Flatten the editor's `details` › `detailsSummary` + `detailsContent` into a
304
- * concept block's body, summary first — as a HEADING.
305
- *
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.
315
- *
316
- * @param {Object} node
317
- * @returns {Array}
318
- */
319
- function detailsBody(node) {
320
- const body = []
321
- for (const child of node.content || []) {
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 || []))
327
- else body.push(child)
328
- }
329
- return body
330
- }
331
-
332
247
  /** A mark that carries nothing and should be removed rather than reported. */
333
248
  const DROP_MARK = Symbol('drop-mark')
334
249
 
@@ -262,35 +262,4 @@ describe('containers — the block form of an inset', () => {
262
262
  const source = '````@Alert{type=info}\nDo not:\n\n```js\nconst x = 1\n```\n````'
263
263
  expect(rt(source)).toBe(source)
264
264
  })
265
-
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.
271
- const text = t => ({ type: 'text', text: t })
272
- const doc = node => proseMirrorToMarkdown({ type: 'doc', content: [node] }).trim()
273
-
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.
277
- expect(doc({ type: 'WarningBlock', attrs: {}, content: [text('Careful.')] }))
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.
287
- expect(doc({
288
- type: 'details',
289
- content: [
290
- { type: 'detailsSummary', content: [text('Breaking '), { type: 'text', text: 'changes', marks: [{ type: 'bold' }] }] },
291
- { type: 'detailsContent', content: [{ type: 'paragraph', content: [text('Body.')] }] },
292
- ],
293
- })).toBe('```md:details\n# Breaking **changes**\n\nBody.\n```')
294
- })
295
-
296
265
  })