inkpen 0.9.0 → 0.9.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53bb272d92be9ebaaa8096f29650efdaf398d94869ba2816a6ce1865934ef052
4
- data.tar.gz: 5d1e03b894fcd2a74f58b04e9d647483bd8536111aecb6d43f13e1b6defbe2bd
3
+ metadata.gz: 92089e260ac3068e3e269e864d4ec9f0294447bbb4e28764f02f5c722debc155
4
+ data.tar.gz: 1b824dd56c5ccd3f1c448cf841ea53db2660617f14a6b4fe5a41ee9b20f15f7e
5
5
  SHA512:
6
- metadata.gz: b54aed81a7ed029d17713ec708326ebe26e55eb1d042cdd6d8d5da68adc972767525efd0e828fff0d4d3930ce82939e5cf901dec6c30c24cd1ec40a483c46d0e
7
- data.tar.gz: d11b40ec2bed5b98667ffca8e8689ea8e97a03ef8d5da060ac530cfd29fb74064297baacca19993f9a94a60413a743d83114bd3fd93c75d25f352e2efc051566
6
+ metadata.gz: 5d6ceb94d0867ab0b0a5194f6a1468454c1ef8b6782b94cd62ce469a15667ca04ad3c764681130c1e148cc823ef0b8cdd770a0822a01ffb402be632300869ce0
7
+ data.tar.gz: 9420e9694ce248bec30176e60e26a3521749a1e7b5463ba14423a58b87df6b18b40a82b82ff5ab9aa1f9146637f8330e32f1abb309744ca89d09346deaa28e89
@@ -1,158 +1,225 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  // ============================================
4
- // LAZY LOADING
4
+ // LAZY LOADING — gated by enabled extensions (spec 02 slice B, 0.9.1)
5
5
  // ============================================
6
- // All TipTap/ProseMirror modules are lazy-loaded when the editor connects.
7
- // This prevents 50+ CDN requests on pages that don't use the editor.
8
- // The modules are cached after first load for subsequent editor instances.
9
-
10
- let cachedModules = null
11
-
12
- async function loadEditorModules() {
13
- if (cachedModules) return cachedModules
14
-
15
- // Load all core modules in parallel
16
- const [
17
- tiptapCore,
18
- tiptapPmModel,
19
- documentExt,
20
- starterKit,
21
- linkExt,
22
- placeholderExt,
23
- imageExt,
24
- tableExt,
25
- tableRowExt,
26
- tableCellExt,
27
- tableHeaderExt,
28
- taskListExt,
29
- taskItemExt,
30
- mentionExt,
31
- codeBlockLowlightExt,
32
- lowlightMod,
33
- typographyExt,
34
- highlightExt,
35
- underlineExt,
36
- subscriptExt,
37
- superscriptExt,
38
- youtubeExt,
39
- characterCountExt,
40
- bubbleMenuExt,
41
- // Inkpen custom extensions
42
- sectionMod,
43
- preformattedMod,
44
- slashCommandsMod,
45
- blockGutterMod,
46
- dragHandleMod,
47
- toggleBlockMod,
48
- columnsMod,
49
- calloutMod,
50
- blockCommandsMod,
51
- enhancedImageMod,
52
- fileAttachmentMod,
53
- embedMod,
54
- advancedTableMod,
55
- tableOfContentsMod,
56
- databaseMod,
57
- documentSectionMod,
58
- contentEmbedMod
59
- ] = await Promise.all([
60
- import("@tiptap/core"),
61
- import("@tiptap/pm/model"),
62
- import("@tiptap/extension-document"),
63
- import("@tiptap/starter-kit"),
64
- import("@tiptap/extension-link"),
65
- import("@tiptap/extension-placeholder"),
66
- import("@tiptap/extension-image"),
67
- import("@tiptap/extension-table"),
68
- import("@tiptap/extension-table-row"),
69
- import("@tiptap/extension-table-cell"),
70
- import("@tiptap/extension-table-header"),
71
- import("@tiptap/extension-task-list"),
72
- import("@tiptap/extension-task-item"),
73
- import("@tiptap/extension-mention"),
74
- import("@tiptap/extension-code-block-lowlight"),
75
- import("lowlight"),
76
- import("@tiptap/extension-typography"),
77
- import("@tiptap/extension-highlight"),
78
- import("@tiptap/extension-underline"),
79
- import("@tiptap/extension-subscript"),
80
- import("@tiptap/extension-superscript"),
81
- import("@tiptap/extension-youtube"),
82
- import("@tiptap/extension-character-count"),
83
- import("@tiptap/extension-bubble-menu"),
84
- // Inkpen custom extensions
85
- import("inkpen/extensions/section"),
86
- import("inkpen/extensions/preformatted"),
87
- import("inkpen/extensions/slash_commands"),
88
- import("inkpen/extensions/block_gutter"),
89
- import("inkpen/extensions/drag_handle"),
90
- import("inkpen/extensions/toggle_block"),
91
- import("inkpen/extensions/columns"),
92
- import("inkpen/extensions/callout"),
93
- import("inkpen/extensions/block_commands"),
94
- import("inkpen/extensions/enhanced_image"),
95
- import("inkpen/extensions/file_attachment"),
96
- import("inkpen/extensions/embed"),
97
- import("inkpen/extensions/advanced_table"),
98
- import("inkpen/extensions/table_of_contents"),
99
- import("inkpen/extensions/database"),
100
- import("inkpen/extensions/document_section"),
101
- import("inkpen/extensions/content_embed")
102
- ])
103
-
104
- cachedModules = {
6
+ // TipTap/ProseMirror modules are dynamically imported when the editor
7
+ // connects. Slice A (0.9.0) dropped static-imports from index.js; this
8
+ // slice (B, 0.9.1) gates the dynamic-import set on the editor's
9
+ // `extensions-value` configuration.
10
+ //
11
+ // Module objects returned by this loader still expose every extension
12
+ // class name as a key — buildExtensions destructures all of them and
13
+ // each conditional (`if (enabledExtensions.includes("X"))`) reads the
14
+ // destructured local. If a gated extension is disabled, its key is
15
+ // simply absent (undefined), the conditional is false, and the local
16
+ // is never used.
17
+ //
18
+ // Real byte savings need esbuild `splitting: true` — slice C. This
19
+ // slice trims runtime extension-construction cost only; the bundle
20
+ // still inlines every module the dynamic imports can reach.
21
+
22
+ const cachedModules = new Map()
23
+
24
+ // Always loaded — required by every editor regardless of enabled set.
25
+ async function loadCoreModules() {
26
+ const [tiptapCore, tiptapPmModel, starterKit, placeholderExt, bubbleMenuExt] =
27
+ await Promise.all([
28
+ import("@tiptap/core"),
29
+ import("@tiptap/pm/model"),
30
+ import("@tiptap/starter-kit"),
31
+ import("@tiptap/extension-placeholder"),
32
+ import("@tiptap/extension-bubble-menu")
33
+ ])
34
+ return {
105
35
  Editor: tiptapCore.Editor,
106
36
  DOMSerializer: tiptapPmModel.DOMSerializer,
107
- Document: documentExt.default,
108
37
  StarterKit: starterKit.default,
109
- Link: linkExt.default,
110
38
  Placeholder: placeholderExt.default,
111
- Image: imageExt.default,
112
- Table: tableExt.default,
113
- TableRow: tableRowExt.default,
114
- TableCell: tableCellExt.default,
115
- TableHeader: tableHeaderExt.default,
116
- TaskList: taskListExt.default,
117
- TaskItem: taskItemExt.default,
118
- Mention: mentionExt.default,
119
- CodeBlockLowlight: codeBlockLowlightExt.default,
120
- common: lowlightMod.common,
121
- createLowlight: lowlightMod.createLowlight,
122
- Typography: typographyExt.default,
123
- Highlight: highlightExt.default,
124
- Underline: underlineExt.default,
125
- Subscript: subscriptExt.default,
126
- Superscript: superscriptExt.default,
127
- Youtube: youtubeExt.default,
128
- CharacterCount: characterCountExt.default,
129
- BubbleMenu: bubbleMenuExt.default,
130
- // Inkpen custom extensions
131
- Section: sectionMod.Section,
132
- Preformatted: preformattedMod.Preformatted,
133
- SlashCommands: slashCommandsMod.SlashCommands,
134
- BlockGutter: blockGutterMod.BlockGutter,
135
- DragHandle: dragHandleMod.DragHandle,
136
- ToggleBlock: toggleBlockMod.ToggleBlock,
137
- ToggleSummary: toggleBlockMod.ToggleSummary,
138
- Columns: columnsMod.Columns,
139
- Column: columnsMod.Column,
140
- Callout: calloutMod.Callout,
141
- BlockCommands: blockCommandsMod.BlockCommands,
142
- EnhancedImage: enhancedImageMod.EnhancedImage,
143
- FileAttachment: fileAttachmentMod.FileAttachment,
144
- Embed: embedMod.Embed,
145
- AdvancedTable: advancedTableMod.AdvancedTable,
146
- AdvancedTableRow: advancedTableMod.AdvancedTableRow,
147
- AdvancedTableCell: advancedTableMod.AdvancedTableCell,
148
- AdvancedTableHeader: advancedTableMod.AdvancedTableHeader,
149
- TableOfContents: tableOfContentsMod.TableOfContents,
150
- Database: databaseMod.Database,
151
- DocumentSection: documentSectionMod.DocumentSection,
152
- ContentEmbed: contentEmbedMod.ContentEmbed
153
- }
154
-
155
- return cachedModules
39
+ BubbleMenu: bubbleMenuExt.default
40
+ }
41
+ }
42
+
43
+ // One loader per gated extension name. Each returns the subset of
44
+ // keys it contributes to the merged modules object. Adding a new
45
+ // extension means: (1) add its name + loader here, (2) destructure
46
+ // the new key in buildExtensions, (3) gate its usage there.
47
+ const EXTENSION_LOADERS = {
48
+ forced_document: async () => {
49
+ const m = await import("@tiptap/extension-document")
50
+ return { Document: m.default }
51
+ },
52
+ link: async () => {
53
+ const m = await import("@tiptap/extension-link")
54
+ return { Link: m.default }
55
+ },
56
+ image: async () => {
57
+ const m = await import("@tiptap/extension-image")
58
+ return { Image: m.default }
59
+ },
60
+ table: async () => {
61
+ const [t, tr, tc, th] = await Promise.all([
62
+ import("@tiptap/extension-table"),
63
+ import("@tiptap/extension-table-row"),
64
+ import("@tiptap/extension-table-cell"),
65
+ import("@tiptap/extension-table-header")
66
+ ])
67
+ return {
68
+ Table: t.default,
69
+ TableRow: tr.default,
70
+ TableCell: tc.default,
71
+ TableHeader: th.default
72
+ }
73
+ },
74
+ task_list: async () => {
75
+ const [tl, ti] = await Promise.all([
76
+ import("@tiptap/extension-task-list"),
77
+ import("@tiptap/extension-task-item")
78
+ ])
79
+ return { TaskList: tl.default, TaskItem: ti.default }
80
+ },
81
+ mention: async () => {
82
+ const m = await import("@tiptap/extension-mention")
83
+ return { Mention: m.default }
84
+ },
85
+ code_block_syntax: async () => {
86
+ const [cb, low] = await Promise.all([
87
+ import("@tiptap/extension-code-block-lowlight"),
88
+ import("lowlight")
89
+ ])
90
+ return {
91
+ CodeBlockLowlight: cb.default,
92
+ common: low.common,
93
+ createLowlight: low.createLowlight
94
+ }
95
+ },
96
+ typography: async () => {
97
+ const m = await import("@tiptap/extension-typography")
98
+ return { Typography: m.default }
99
+ },
100
+ highlight: async () => {
101
+ const m = await import("@tiptap/extension-highlight")
102
+ return { Highlight: m.default }
103
+ },
104
+ underline: async () => {
105
+ const m = await import("@tiptap/extension-underline")
106
+ return { Underline: m.default }
107
+ },
108
+ subscript: async () => {
109
+ const m = await import("@tiptap/extension-subscript")
110
+ return { Subscript: m.default }
111
+ },
112
+ superscript: async () => {
113
+ const m = await import("@tiptap/extension-superscript")
114
+ return { Superscript: m.default }
115
+ },
116
+ youtube: async () => {
117
+ const m = await import("@tiptap/extension-youtube")
118
+ return { Youtube: m.default }
119
+ },
120
+ character_count: async () => {
121
+ const m = await import("@tiptap/extension-character-count")
122
+ return { CharacterCount: m.default }
123
+ },
124
+ section: async () => {
125
+ const m = await import("inkpen/extensions/section")
126
+ return { Section: m.Section }
127
+ },
128
+ preformatted: async () => {
129
+ const m = await import("inkpen/extensions/preformatted")
130
+ return { Preformatted: m.Preformatted }
131
+ },
132
+ slash_commands: async () => {
133
+ const m = await import("inkpen/extensions/slash_commands")
134
+ return { SlashCommands: m.SlashCommands }
135
+ },
136
+ block_gutter: async () => {
137
+ const m = await import("inkpen/extensions/block_gutter")
138
+ return { BlockGutter: m.BlockGutter }
139
+ },
140
+ drag_handle: async () => {
141
+ const m = await import("inkpen/extensions/drag_handle")
142
+ return { DragHandle: m.DragHandle }
143
+ },
144
+ toggle_block: async () => {
145
+ const m = await import("inkpen/extensions/toggle_block")
146
+ return { ToggleBlock: m.ToggleBlock, ToggleSummary: m.ToggleSummary }
147
+ },
148
+ columns: async () => {
149
+ const m = await import("inkpen/extensions/columns")
150
+ return { Columns: m.Columns, Column: m.Column }
151
+ },
152
+ callout: async () => {
153
+ const m = await import("inkpen/extensions/callout")
154
+ return { Callout: m.Callout }
155
+ },
156
+ block_commands: async () => {
157
+ const m = await import("inkpen/extensions/block_commands")
158
+ return { BlockCommands: m.BlockCommands }
159
+ },
160
+ enhanced_image: async () => {
161
+ const m = await import("inkpen/extensions/enhanced_image")
162
+ return { EnhancedImage: m.EnhancedImage }
163
+ },
164
+ file_attachment: async () => {
165
+ const m = await import("inkpen/extensions/file_attachment")
166
+ return { FileAttachment: m.FileAttachment }
167
+ },
168
+ embed: async () => {
169
+ const m = await import("inkpen/extensions/embed")
170
+ return { Embed: m.Embed }
171
+ },
172
+ advanced_table: async () => {
173
+ const m = await import("inkpen/extensions/advanced_table")
174
+ return {
175
+ AdvancedTable: m.AdvancedTable,
176
+ AdvancedTableRow: m.AdvancedTableRow,
177
+ AdvancedTableCell: m.AdvancedTableCell,
178
+ AdvancedTableHeader: m.AdvancedTableHeader
179
+ }
180
+ },
181
+ table_of_contents: async () => {
182
+ const m = await import("inkpen/extensions/table_of_contents")
183
+ return { TableOfContents: m.TableOfContents }
184
+ },
185
+ database: async () => {
186
+ const m = await import("inkpen/extensions/database")
187
+ return { Database: m.Database }
188
+ },
189
+ document_section: async () => {
190
+ const m = await import("inkpen/extensions/document_section")
191
+ return { DocumentSection: m.DocumentSection }
192
+ },
193
+ content_embed: async () => {
194
+ const m = await import("inkpen/extensions/content_embed")
195
+ return { ContentEmbed: m.ContentEmbed }
196
+ }
197
+ }
198
+
199
+ // Exported for tests so they can mock loaders or assert the registry's
200
+ // surface. Not part of the public gem API; the controller is the only
201
+ // production consumer.
202
+ export const __EXTENSION_LOADER_NAMES__ = Object.keys(EXTENSION_LOADERS)
203
+ export const __loadEditorModulesForTest = loadEditorModules
204
+
205
+ async function loadEditorModules(enabledExtensions = []) {
206
+ const enabledSet = new Set(enabledExtensions)
207
+ const cacheKey = [...enabledSet].sort().join(",") || "(empty)"
208
+ if (cachedModules.has(cacheKey)) return cachedModules.get(cacheKey)
209
+
210
+ const core = await loadCoreModules()
211
+
212
+ const gatedPromises = []
213
+ for (const [name, loader] of Object.entries(EXTENSION_LOADERS)) {
214
+ if (enabledSet.has(name)) {
215
+ gatedPromises.push(loader())
216
+ }
217
+ }
218
+ const gatedResults = await Promise.all(gatedPromises)
219
+
220
+ const modules = Object.assign({}, core, ...gatedResults)
221
+ cachedModules.set(cacheKey, modules)
222
+ return modules
156
223
  }
157
224
 
158
225
  // Extensions loaded lazily to prevent import failures from breaking the editor
@@ -332,7 +399,7 @@ export default class extends Controller {
332
399
 
333
400
  async initializeEditor() {
334
401
  // Lazy-load all TipTap modules
335
- this.modules = await loadEditorModules()
402
+ this.modules = await loadEditorModules(this.extensionsValue)
336
403
  const { Editor } = this.modules
337
404
 
338
405
  const extensions = await this.buildExtensions()
@@ -291,11 +291,11 @@ function serializeInlineContent(node) {
291
291
  const iterate = (items) => {
292
292
  if (Array.isArray(items)) {
293
293
  for (const item of items) {
294
- result += serializeTextWithMarks(item)
294
+ result += serializeInlineNode(item)
295
295
  }
296
296
  } else if (typeof items.forEach === "function") {
297
297
  items.forEach(item => {
298
- result += serializeTextWithMarks(item)
298
+ result += serializeInlineNode(item)
299
299
  })
300
300
  }
301
301
  }
@@ -304,6 +304,16 @@ function serializeInlineContent(node) {
304
304
  return result
305
305
  }
306
306
 
307
+ function serializeInlineNode(node) {
308
+ if (!node) return ""
309
+
310
+ const type = node.type?.name || node.type
311
+ if (type === "text") return serializeTextWithMarks(node)
312
+ if (type === "hardBreak") return "\n"
313
+
314
+ return serializeNode(node).trim()
315
+ }
316
+
307
317
  /**
308
318
  * Serialize text node with marks
309
319
  */
@@ -316,8 +326,24 @@ function serializeTextWithMarks(node) {
316
326
  return text
317
327
  }
318
328
 
319
- // Apply marks in order (innermost first)
320
- for (const mark of node.marks) {
329
+ const markName = mark => mark.type?.name || mark.type
330
+ const hasBold = node.marks.some(mark => ["bold", "strong"].includes(markName(mark)))
331
+ const hasItalic = node.marks.some(mark => ["italic", "em"].includes(markName(mark)))
332
+
333
+ if (hasBold && hasItalic) {
334
+ text = `***${text}***`
335
+ } else if (hasBold) {
336
+ text = MARK_SERIALIZERS.bold(text)
337
+ } else if (hasItalic) {
338
+ text = MARK_SERIALIZERS.italic(text)
339
+ }
340
+
341
+ const remainingMarks = node.marks.filter(mark => {
342
+ const name = markName(mark)
343
+ return !["bold", "strong", "italic", "em"].includes(name)
344
+ })
345
+
346
+ for (const mark of remainingMarks) {
321
347
  const serializer = MARK_SERIALIZERS[mark.type?.name || mark.type]
322
348
  if (serializer) {
323
349
  text = serializer(text, mark)
@@ -425,7 +451,8 @@ function serializeBlockquote(node, options) {
425
451
  */
426
452
  function serializeCodeBlock(node) {
427
453
  const language = node.attrs?.language || ""
428
- return "```" + language + "\n" + node.textContent + "\n```"
454
+ const content = (node.textContent || "").replace(/\n$/, "")
455
+ return "```" + language + "\n" + content + "\n```"
429
456
  }
430
457
 
431
458
  /**
@@ -470,7 +497,7 @@ function serializeTable(node, options) {
470
497
  const rowChildren = row.content?.content || row.content || []
471
498
 
472
499
  iterate(rowChildren, (cell, cellIndex) => {
473
- const content = serializeInlineContent(cell).trim() || " "
500
+ const content = serializeTableCell(cell, options)
474
501
  cells.push(content)
475
502
 
476
503
  // Track alignment from first row cells
@@ -500,6 +527,15 @@ function serializeTable(node, options) {
500
527
  return rows.join("\n") + "\n"
501
528
  }
502
529
 
530
+ function serializeTableCell(cell, options) {
531
+ const content = serializeChildren(cell, options)
532
+ .trim()
533
+ .replace(/\n+/g, "<br>")
534
+ .replace(/\s*\|\s*/g, "\\|")
535
+
536
+ return content || " "
537
+ }
538
+
503
539
  /**
504
540
  * Serialize callout to GFM-style alert
505
541
  */