@phuong-tran-redoc/document-engine-core 0.1.0 → 0.1.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.
Files changed (46) hide show
  1. package/README.md +105 -115
  2. package/THIRD-PARTY-NOTICES.txt +67 -0
  3. package/index.d.ts +1 -0
  4. package/index.esm.js +2238 -0
  5. package/package.json +21 -5
  6. package/src/__tests__/helpers/editor-factory.d.ts +0 -13
  7. package/src/__tests__/helpers/editor-factory.js +0 -25
  8. package/src/constants/indent.constant.js +0 -1
  9. package/src/constants/index.js +0 -1
  10. package/src/constants/table.constant.js +0 -1
  11. package/src/extensions/clear-content.extension.js +0 -9
  12. package/src/extensions/indent.extension.js +0 -59
  13. package/src/extensions/index.js +0 -8
  14. package/src/extensions/ordered-list.extension.js +0 -47
  15. package/src/extensions/reset-format.extension.js +0 -9
  16. package/src/extensions/reset-on-enter.extension.js +0 -37
  17. package/src/extensions/restricted-editing.extension.js +0 -376
  18. package/src/extensions/table-resizing.extension.js +0 -164
  19. package/src/extensions/table-style.extension.js +0 -481
  20. package/src/extensions/text-case.extension.js +0 -39
  21. package/src/index.js +0 -9
  22. package/src/kit/default-extensions.js +0 -86
  23. package/src/kit/generate-html.js +0 -40
  24. package/src/kit/index.js +0 -2
  25. package/src/migrations/doc-migrations.js +0 -24
  26. package/src/migrations/index.js +0 -3
  27. package/src/migrations/migrate-doc.js +0 -32
  28. package/src/migrations/types.js +0 -4
  29. package/src/models/color.model.js +0 -30
  30. package/src/models/index.js +0 -1
  31. package/src/nodes/dynamic-field.node.js +0 -141
  32. package/src/nodes/heading.node.js +0 -13
  33. package/src/nodes/image-ref.node.js +0 -138
  34. package/src/nodes/index.js +0 -4
  35. package/src/nodes/page-break.node.js +0 -52
  36. package/src/types/index.js +0 -2
  37. package/src/types/ordered-list.type.js +0 -2
  38. package/src/types/text-case.type.js +0 -1
  39. package/src/types/tiptap-html-server.d.js +0 -10
  40. package/src/utils/color.util.js +0 -31
  41. package/src/utils/dom.util.js +0 -33
  42. package/src/utils/index.js +0 -4
  43. package/src/utils/table.util.js +0 -95
  44. package/src/utils/text.util.js +0 -46
  45. package/src/views/block-handler.js +0 -311
  46. package/src/views/index.js +0 -1
package/src/index.js DELETED
@@ -1,9 +0,0 @@
1
- export * from './constants';
2
- export * from './extensions';
3
- export * from './kit';
4
- export * from './migrations';
5
- export * from './models';
6
- export * from './nodes';
7
- export * from './types';
8
- export * from './utils';
9
- export * from './views';
@@ -1,86 +0,0 @@
1
- import Blockquote from '@tiptap/extension-blockquote';
2
- import Bold from '@tiptap/extension-bold';
3
- import Code from '@tiptap/extension-code';
4
- import CodeBlock from '@tiptap/extension-code-block';
5
- import { Document } from '@tiptap/extension-document';
6
- import { HardBreak } from '@tiptap/extension-hard-break';
7
- import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
8
- import { Image } from '@tiptap/extension-image';
9
- import Italic from '@tiptap/extension-italic';
10
- import Link from '@tiptap/extension-link';
11
- import { BulletList, ListItem } from '@tiptap/extension-list';
12
- import { Paragraph } from '@tiptap/extension-paragraph';
13
- import Strike from '@tiptap/extension-strike';
14
- import { Subscript } from '@tiptap/extension-subscript';
15
- import { Superscript } from '@tiptap/extension-superscript';
16
- import { Text } from '@tiptap/extension-text';
17
- import { TextAlign } from '@tiptap/extension-text-align';
18
- import { TextStyleKit } from '@tiptap/extension-text-style';
19
- import Underline from '@tiptap/extension-underline';
20
- import { CustomOrderedList } from '../extensions/ordered-list.extension';
21
- import { StyledTableKit } from '../extensions/table-style.extension';
22
- import { DynamicField } from '../nodes/dynamic-field.node';
23
- import { NotumHeading } from '../nodes/heading.node';
24
- import { ImageRef } from '../nodes/image-ref.node';
25
- import { PageBreak } from '../nodes/page-break.node';
26
- /**
27
- * Canonical default extension list for headless (Node-safe) serialization.
28
- *
29
- * This is the schema half of the editor's runtime kit (`DocumentEngineKit` in
30
- * the Angular wrapper): every node, mark, and HTML-affecting attribute the
31
- * editor can persist, so {@link generateHTML} can turn stored document JSON back
32
- * into HTML without a browser.
33
- *
34
- * It deliberately omits editing-only plugins (history/undo-redo, drop/gap
35
- * cursors, placeholder, character count, list keymaps, trailing node). Those
36
- * shape the live editing experience but contribute nothing to serialized
37
- * output, and some attach view-level behavior that has no meaning headless.
38
- *
39
- * Only `renderHTML` is exercised during serialization — node views (which touch
40
- * the DOM, e.g. `DynamicField.addNodeView`) are never invoked here, so this list
41
- * is safe to import and run in Node.
42
- *
43
- * Callers needing a different schema (e.g. restricted-editing regions, or a
44
- * trimmed set) should pass their own array to {@link generateHTML} instead.
45
- */ export const defaultExtensions = [
46
- // Structure
47
- Document,
48
- Paragraph,
49
- Text,
50
- NotumHeading,
51
- HardBreak,
52
- HorizontalRule,
53
- Blockquote,
54
- CodeBlock,
55
- // Lists
56
- BulletList,
57
- ListItem,
58
- CustomOrderedList,
59
- // Marks
60
- Bold,
61
- Italic,
62
- Underline,
63
- Strike,
64
- Code,
65
- Link,
66
- Subscript,
67
- Superscript,
68
- TextStyleKit,
69
- // Block-level alignment (emits `text-align` only when explicitly set).
70
- // NOTE: `Indent` is intentionally excluded — its `renderHTML` emits an empty
71
- // `style=""` on every block even when unset, which pollutes otherwise-semantic
72
- // output. Consumers that persist indentation can pass a custom extension array.
73
- TextAlign.configure({
74
- types: [
75
- 'paragraph',
76
- 'heading'
77
- ]
78
- }),
79
- // Content nodes
80
- Image,
81
- ImageRef,
82
- PageBreak,
83
- DynamicField,
84
- // Tables (table / row / header / cell nodes)
85
- StyledTableKit
86
- ];
@@ -1,40 +0,0 @@
1
- import { defaultExtensions } from './default-extensions';
2
- /**
3
- * Serialize a Tiptap/ProseMirror document (JSON) to an HTML string.
4
- *
5
- * Environment-aware and Node-safe. Tiptap ships two serializers and the wrong
6
- * one throws:
7
- * - `@tiptap/html` needs a real `document` (browser / jsdom).
8
- * - `@tiptap/html/server` is backed by happy-dom and runs headlessly in Node.
9
- *
10
- * The serializer is chosen at call time from whether a DOM is present, and
11
- * imported lazily — so the heavy happy-dom build is never pulled into a browser
12
- * editor bundle that only ever edits (and never serializes server-side), and
13
- * Node backends never touch the browser-only build. This is why the function is
14
- * async.
15
- *
16
- * @param doc The document as ProseMirror JSON (a `doc` node).
17
- * @param extensions Schema to serialize against. Defaults to
18
- * {@link defaultExtensions} (the editor's canonical kit). Pass
19
- * a custom array when your documents use a different schema.
20
- * @returns Semantic HTML. Presentation styling and sanitization (e.g. DOMPurify)
21
- * are the consumer's responsibility — this helper does neither.
22
- */ export async function generateHTML(doc, extensions = defaultExtensions) {
23
- if (typeof document !== 'undefined') {
24
- // browser / jsdom: uses the ambient DOM
25
- const serializer = await import('@tiptap/html');
26
- return serializer.generateHTML(doc, extensions);
27
- }
28
- // Node: happy-dom backed, no real DOM needed. The specifier is built at runtime
29
- // (not a static string literal) and flagged for webpack/vite so browser
30
- // bundlers (esbuild/webpack/vite) can't statically resolve it and therefore
31
- // never pull happy-dom — and its Node built-ins — into a client bundle. The
32
- // `typeof document` guard means this branch never executes in a browser anyway.
33
- const serverEntry = [
34
- '@tiptap',
35
- 'html',
36
- 'server'
37
- ].join('/');
38
- const serializer = await import(/* webpackIgnore: true */ /* @vite-ignore */ serverEntry);
39
- return serializer.generateHTML(doc, extensions);
40
- }
package/src/kit/index.js DELETED
@@ -1,2 +0,0 @@
1
- export * from './default-extensions';
2
- export * from './generate-html';
@@ -1,24 +0,0 @@
1
- /**
2
- * The current schema version. `EditorDocument`s authored by this build of the
3
- * editor carry this value, and {@link migrateDoc} upgrades older documents up to
4
- * it.
5
- *
6
- * Bump this by exactly one whenever a schema change requires transforming stored
7
- * content, and add the matching entry (keyed by the previous version) to
8
- * {@link docMigrations}.
9
- */ export const LATEST_SCHEMA_VERSION = 1;
10
- /**
11
- * Ordered migration steps, keyed by the source version they upgrade from.
12
- *
13
- * v0.1.0 ships an identity (empty) set: there is nothing to migrate yet, but the
14
- * registry, walker ({@link migrateDoc}), and version field exist so future
15
- * schema bumps are non-breaking. To add one, e.g. for the 1 -> 2 bump:
16
- *
17
- * ```ts
18
- * export const docMigrations: DocMigrationRegistry = {
19
- * 1: (doc) => ({ schemaVersion: 2, content: transformV1ToV2(doc.content) }),
20
- * };
21
- * ```
22
- *
23
- * Each entry must be pure (no mutation, no browser APIs).
24
- */ export const docMigrations = {};
@@ -1,3 +0,0 @@
1
- export * from './types';
2
- export * from './doc-migrations';
3
- export * from './migrate-doc';
@@ -1,32 +0,0 @@
1
- import { docMigrations, LATEST_SCHEMA_VERSION } from './doc-migrations';
2
- /**
3
- * Upgrade a stored document to {@link LATEST_SCHEMA_VERSION} by applying each
4
- * registered migration in order.
5
- *
6
- * Pure and idempotent: a document already at the latest version is returned
7
- * unchanged (a re-stamped copy), so batch runs are safe to re-run. A document
8
- * with a missing/invalid `schemaVersion` is treated as version 1 (the earliest).
9
- *
10
- * @param doc The document to migrate.
11
- * @param migrations Registry to apply. Defaults to {@link docMigrations}; the
12
- * override exists for testing the walker in isolation.
13
- * @param latest Target version. Defaults to {@link LATEST_SCHEMA_VERSION}.
14
- * @returns A new `EditorDocument` at `latest`.
15
- */ export function migrateDoc(doc, migrations = docMigrations, latest = LATEST_SCHEMA_VERSION) {
16
- let current = {
17
- ...doc,
18
- schemaVersion: Number.isInteger(doc.schemaVersion) && doc.schemaVersion >= 1 ? doc.schemaVersion : 1
19
- };
20
- while(current.schemaVersion < latest){
21
- const migrate = migrations[current.schemaVersion];
22
- if (!migrate) {
23
- throw new Error(`[migrateDoc] No migration registered for schema version ${current.schemaVersion} ` + `(target ${latest}).`);
24
- }
25
- const next = migrate(current);
26
- if (next.schemaVersion <= current.schemaVersion) {
27
- throw new Error(`[migrateDoc] Migration from version ${current.schemaVersion} did not advance the ` + `version (got ${next.schemaVersion}); migrations must be strictly increasing.`);
28
- }
29
- current = next;
30
- }
31
- return current;
32
- }
@@ -1,4 +0,0 @@
1
- /**
2
- * Ordered registry of migration steps, keyed by the *source* version each step
3
- * upgrades from. Entry `N` takes a v`N` document and returns a v`N + 1` one.
4
- */ export { };
@@ -1,30 +0,0 @@
1
- import { normalizeColor } from '../utils';
2
- export class Color {
3
- static from(value, name, type) {
4
- return new Color(value, name, type);
5
- }
6
- static fromArray(colors) {
7
- return colors.map((color)=>{
8
- if (typeof color === 'string') {
9
- return new Color(color);
10
- }
11
- return new Color(color.value, color.name, color.type);
12
- });
13
- }
14
- equals(color) {
15
- return this.value === color.value;
16
- }
17
- is(color) {
18
- return this.value === normalizeColor(color);
19
- }
20
- constructor(value, name, type){
21
- const normalizedColor = normalizeColor(value);
22
- if (!normalizedColor) {
23
- throw new Error('Invalid or unsupported color.');
24
- }
25
- this.value = normalizedColor;
26
- this.name = name ?? normalizedColor;
27
- this.type = type;
28
- Object.freeze(this);
29
- }
30
- }
@@ -1 +0,0 @@
1
- export * from './color.model';
@@ -1,141 +0,0 @@
1
- import { Node, mergeAttributes } from '@tiptap/core';
2
- /**
3
- * Dynamic Field Node Extension
4
- *
5
- * This extension creates atomic, non-editable inline nodes that represent
6
- * dynamic fields (e.g., {{customer_name}}) in document templates.
7
- * These fields can be programmatically replaced with actual data later.
8
- */ export const DynamicField = Node.create({
9
- name: 'dynamicField',
10
- /**
11
- * Node group configuration
12
- * - 'inline': Makes the node behave as inline content
13
- * - 'atom': true is critical for non-editable, atomic behavior
14
- * - 'selectable': true allows users to select the entire node
15
- * - 'draggable': false prevents users from dragging the node
16
- */ group: 'inline',
17
- inline: true,
18
- atom: true,
19
- selectable: true,
20
- draggable: false,
21
- /**
22
- * Define the attributes that this node can store
23
- */ addAttributes () {
24
- return {
25
- fieldId: {
26
- default: null,
27
- parseHTML: (element)=>element.getAttribute('data-field-id'),
28
- renderHTML: (attributes)=>{
29
- if (!attributes['fieldId']) return {};
30
- return {
31
- 'data-field-id': attributes['fieldId']
32
- };
33
- }
34
- },
35
- label: {
36
- default: null,
37
- parseHTML: (element)=>element.getAttribute('data-label'),
38
- renderHTML: (attributes)=>{
39
- if (!attributes['label']) return {};
40
- return {
41
- 'data-label': attributes['label']
42
- };
43
- }
44
- }
45
- };
46
- },
47
- /**
48
- * Parse HTML to recognize dynamic field spans when loading/pasting content
49
- */ parseHTML () {
50
- return [
51
- // ----------------------------------------------------------
52
- // RULE 1: Standard Tiptap Format (Ưu tiên cao nhất)
53
- // ----------------------------------------------------------
54
- {
55
- tag: 'span[data-field-id]'
56
- },
57
- // ----------------------------------------------------------
58
- // RULE 2: Legacy CKEditor Format (Migration Layer)
59
- // ----------------------------------------------------------
60
- {
61
- // Selector CSS để nhận diện node của CKEditor
62
- tag: 'span.red-dynamic-field',
63
- // Hàm này chạy khi Tiptap tìm thấy tag trên
64
- getAttrs: (node)=>{
65
- if (typeof node === 'string') return false;
66
- const element = node;
67
- // 1. Trích xuất ID từ nội dung text: "{{ref}}" -> "ref"
68
- const rawText = element.innerText || element.textContent || '';
69
- // Regex để xóa dấu {{ và }} và khoảng trắng thừa
70
- const id = rawText.replace(/{{|}}/g, '').trim();
71
- // 2. Trích xuất Label từ attribute cũ
72
- // CKEditor dùng 'value', 'dynamicfieldname' hoặc 'name'
73
- const label = element.getAttribute('value') || element.getAttribute('dynamicfieldname') || element.getAttribute('name') || id; // Fallback nếu không tìm thấy label
74
- // Trả về object khớp với cấu trúc 'addAttributes' ở trên
75
- return {
76
- fieldId: id,
77
- label: label
78
- };
79
- }
80
- }
81
- ];
82
- },
83
- /**
84
- * Render the node as HTML
85
- * Creates a span element with Tailwind classes for styling
86
- * and stores attributes in data-* attributes for robustness
87
- */ renderHTML ({ HTMLAttributes, node }) {
88
- const { fieldId, label } = node.attrs;
89
- return [
90
- 'span',
91
- mergeAttributes(HTMLAttributes, {
92
- class: 'dynamic-field red-dynamic-field',
93
- 'data-field-id': fieldId,
94
- 'data-label': label
95
- }),
96
- // label || '',
97
- fieldId ? `{{${fieldId}}}` : ''
98
- ];
99
- },
100
- /**
101
- * NODE VIEW: Đây là phần giúp hiển thị giao diện giống CKEditor Widget
102
- * Thay vì render ra text thuần, ta render ra một DOM element được style sẵn
103
- */ addNodeView () {
104
- return ({ node })=>{
105
- // 1. Tạo thẻ bao bọc (Wrapper)
106
- const dom = document.createElement('span');
107
- // 2. Thêm class để style (Dùng Tailwind như techstack của bạn )
108
- // Bạn có thể thêm class 'ck-widget' nếu muốn dùng lại CSS cũ
109
- dom.classList.add('dynamic-field', 'red-dynamic-field');
110
- // 3. Gán dữ liệu label vào nội dung hiển thị
111
- const { label, fieldId } = node.attrs;
112
- dom.textContent = label || fieldId || 'Unknown Field';
113
- return {
114
- dom
115
- };
116
- };
117
- },
118
- /**
119
- * Add custom commands for inserting dynamic fields
120
- */ addCommands () {
121
- return {
122
- insertDynamicField: (attributes)=>({ chain })=>{
123
- if (!attributes.fieldId || !attributes.label) {
124
- console.error('[DynamicField] Both fieldId and label are required.');
125
- return false;
126
- }
127
- return chain().insertContent({
128
- type: this.name,
129
- attrs: attributes
130
- }).run();
131
- }
132
- };
133
- },
134
- /**
135
- * Default options
136
- */ addOptions () {
137
- return {
138
- HTMLAttributes: {}
139
- };
140
- }
141
- });
@@ -1,13 +0,0 @@
1
- import { Heading } from '@tiptap/extension-heading';
2
- import '@tiptap/extension-paragraph';
3
- export const NotumHeading = Heading.extend({
4
- addCommands () {
5
- return {
6
- ...this.parent?.(),
7
- removeHeading: ()=>({ editor, chain })=>{
8
- if (!editor.isActive('heading')) return false;
9
- return chain().setParagraph().run();
10
- }
11
- };
12
- }
13
- });
@@ -1,138 +0,0 @@
1
- import { Node, mergeAttributes } from '@tiptap/core';
2
- /**
3
- * `image-ref` — a semantic, URL-free reference to a media asset.
4
- *
5
- * Renders a `<figure data-block="image-ref" data-image-id="…">` (with an optional
6
- * `<figcaption>`), decoupling document content from the resolved image URL. The
7
- * consumer hydrates the figure by attribute (`data-image-id`) at render time —
8
- * the document itself never embeds a URL or any presentation. In the editor the
9
- * node shows a placeholder (it has no URL to display); resolving and showing the
10
- * real image is the consumer's / media-picker's job.
11
- */ export const ImageRef = Node.create({
12
- name: 'imageRef',
13
- group: 'block',
14
- atom: true,
15
- selectable: true,
16
- draggable: true,
17
- addOptions () {
18
- return {
19
- HTMLAttributes: {}
20
- };
21
- },
22
- addAttributes () {
23
- return {
24
- imageId: {
25
- default: null,
26
- parseHTML: (element)=>element.getAttribute('data-image-id'),
27
- renderHTML: (attributes)=>{
28
- if (!attributes['imageId']) return {};
29
- return {
30
- 'data-image-id': attributes['imageId']
31
- };
32
- }
33
- },
34
- caption: {
35
- default: null,
36
- // Caption is rendered as <figcaption> text content (below), not a data
37
- // attribute — so parse it back from the figcaption child for round-trip.
38
- parseHTML: (element)=>{
39
- const figcaption = element.querySelector(':scope > figcaption');
40
- return figcaption?.textContent?.trim() || null;
41
- },
42
- renderHTML: ()=>({})
43
- },
44
- captionPosition: {
45
- default: null,
46
- // Normalize to the typed union: absent attribute stays `null` (preserves
47
- // the byte-identical round-trip), any present-but-unknown value coerces to
48
- // `'bottom'` so a non-`'top' | 'bottom'` string can never leak through.
49
- parseHTML: (element)=>{
50
- const raw = element.getAttribute('data-caption-position');
51
- if (raw === null) return null;
52
- return raw === 'top' ? 'top' : 'bottom';
53
- },
54
- renderHTML: (attributes)=>{
55
- if (!attributes['captionPosition']) return {};
56
- return {
57
- 'data-caption-position': attributes['captionPosition']
58
- };
59
- }
60
- }
61
- };
62
- },
63
- parseHTML () {
64
- return [
65
- {
66
- tag: 'figure[data-block="image-ref"]'
67
- }
68
- ];
69
- },
70
- renderHTML ({ HTMLAttributes, node }) {
71
- const { caption } = node.attrs;
72
- const figure = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
73
- 'data-block': 'image-ref'
74
- });
75
- // No <img> is emitted — the asset is URL-free. The caption order relative to
76
- // the (not-yet-resolved) image is carried by `data-caption-position` for the
77
- // consumer to honor at hydration time; the figure itself just holds the text.
78
- if (!caption) {
79
- return [
80
- 'figure',
81
- figure
82
- ];
83
- }
84
- const figcaption = [
85
- 'figcaption',
86
- {},
87
- caption
88
- ];
89
- return [
90
- 'figure',
91
- figure,
92
- figcaption
93
- ];
94
- },
95
- /**
96
- * Editor-only placeholder. The DOM here is chrome (never serialized — that is
97
- * `renderHTML`'s job); it just shows which media the block references.
98
- */ addNodeView () {
99
- return ({ node })=>{
100
- const { imageId, caption, captionPosition } = node.attrs;
101
- const dom = document.createElement('figure');
102
- dom.classList.add('image-ref-placeholder');
103
- dom.setAttribute('data-block', 'image-ref');
104
- if (imageId) dom.setAttribute('data-image-id', imageId);
105
- if (captionPosition) dom.setAttribute('data-caption-position', captionPosition);
106
- dom.style.cssText = 'margin:1rem 0;display:flex;flex-direction:column;gap:.5rem;';
107
- const box = document.createElement('div');
108
- box.style.cssText = 'display:flex;align-items:center;gap:.5rem;padding:1rem;border:1px dashed currentColor;' + 'border-radius:.5rem;opacity:.65;font-size:.875rem;justify-content:center;';
109
- box.textContent = `🖼 image-ref · id: ${imageId || '—'}`;
110
- const figcaption = document.createElement('figcaption');
111
- figcaption.style.cssText = 'font-size:.8125rem;opacity:.7;text-align:center;';
112
- if (caption) figcaption.textContent = caption;
113
- if (caption && captionPosition === 'top') {
114
- dom.append(figcaption, box);
115
- } else {
116
- dom.append(box);
117
- if (caption) dom.append(figcaption);
118
- }
119
- return {
120
- dom
121
- };
122
- };
123
- },
124
- addCommands () {
125
- return {
126
- insertImageRef: (attributes)=>({ chain })=>{
127
- if (!attributes.imageId) {
128
- console.error('[ImageRef] imageId is required.');
129
- return false;
130
- }
131
- return chain().insertContent({
132
- type: this.name,
133
- attrs: attributes
134
- }).run();
135
- }
136
- };
137
- }
138
- });
@@ -1,4 +0,0 @@
1
- export * from './heading.node';
2
- export * from './dynamic-field.node';
3
- export * from './page-break.node';
4
- export * from './image-ref.node';
@@ -1,52 +0,0 @@
1
- import { Node, mergeAttributes } from '@tiptap/core';
2
- export const PageBreak = Node.create({
3
- name: 'pageBreak',
4
- group: 'block',
5
- atom: true,
6
- selectable: true,
7
- // addNodeView() {
8
- // return createPageBreakNodeView;
9
- // },
10
- parseHTML () {
11
- return [
12
- // ----------------------------------------------------------
13
- // RULE 1: Standard Tiptap Format (Ưu tiên cao nhất)
14
- // ----------------------------------------------------------
15
- {
16
- tag: 'div[data-page-break]'
17
- },
18
- // ----------------------------------------------------------
19
- // RULE 2: CKEditor Format (Ưu tiên thấp nhất)
20
- // ----------------------------------------------------------
21
- {
22
- tag: 'div.page-break'
23
- }
24
- ];
25
- },
26
- renderHTML ({ HTMLAttributes }) {
27
- return [
28
- 'div',
29
- mergeAttributes(HTMLAttributes, {
30
- 'data-page-break': 'true',
31
- class: 'document-engine-page-break',
32
- style: 'break-before: page; page-break-before: always;'
33
- }),
34
- [
35
- 'span',
36
- {
37
- class: 'document-engine-page-break-label'
38
- },
39
- 'Page break'
40
- ]
41
- ];
42
- },
43
- addCommands () {
44
- return {
45
- insertPageBreak: ()=>({ chain })=>{
46
- return chain().insertContent({
47
- type: this.name
48
- }).run();
49
- }
50
- };
51
- }
52
- });
@@ -1,2 +0,0 @@
1
- export * from './ordered-list.type';
2
- export * from './text-case.type';
@@ -1,2 +0,0 @@
1
- // TODO CKEditor: Add lower-roman, decimal-leading-zero, upper-roman, lower-latin, upper-latin
2
- export { };
@@ -1 +0,0 @@
1
- export { };
@@ -1,10 +0,0 @@
1
- /**
2
- * Type shim for the `@tiptap/html/server` subpath.
3
- *
4
- * `@tiptap/html` only exposes its Node-safe (`/server`) entry through the
5
- * package `exports` map. This library compiles under `moduleResolution: node10`,
6
- * which ignores `exports` for type resolution, so TypeScript cannot find the
7
- * subpath's declarations on its own. The runtime (Jest/Node) resolves `/server`
8
- * via the exports map fine — this declaration only satisfies the type checker by
9
- * pointing it at the same public types as the bare package.
10
- */
@@ -1,31 +0,0 @@
1
- /**
2
- * Normalizes a color value to lowercase hex format
3
- * Supports hex (#RGB, #RRGGBB) and rgb(r, g, b) formats
4
- *
5
- * @param color - Color value to normalize
6
- * @returns Normalized hex color or null if invalid
7
- *
8
- * @example
9
- * normalizeColor('#FF0000') // '#ff0000'
10
- * normalizeColor('rgb(255, 0, 0)') // '#ff0000'
11
- * normalizeColor('#F00') // '#f00'
12
- * normalizeColor('invalid') // null
13
- */ export function normalizeColor(color) {
14
- if (!color) return null;
15
- const trimmed = color.trim().toLowerCase();
16
- if (trimmed === 'transparent') return trimmed;
17
- // Check if it's a valid hex color
18
- if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) {
19
- return trimmed;
20
- }
21
- // Try to parse RGB
22
- const rgbMatch = trimmed.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
23
- if (rgbMatch) {
24
- const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1], 10)));
25
- const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2], 10)));
26
- const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3], 10)));
27
- const toHex = (n)=>n.toString(16).padStart(2, '0');
28
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
29
- }
30
- return null;
31
- }