@phuong-tran-redoc/document-engine-core 0.0.41 → 0.1.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.
@@ -0,0 +1,40 @@
1
+ import { Node } from '@tiptap/core';
2
+ /**
3
+ * Where the caption sits relative to the image.
4
+ */
5
+ export type ImageRefCaptionPosition = 'top' | 'bottom';
6
+ /**
7
+ * Attributes carried by an `image-ref` node.
8
+ *
9
+ * Only `imageId` is required. The node is intentionally **presentation-free**:
10
+ * it stores an opaque media id, never a URL or width/style. The real responsive
11
+ * image is resolved downstream (consumer side) from `imageId`.
12
+ */
13
+ export interface ImageRefAttributes {
14
+ imageId: string;
15
+ /** Plain text only — serialized as `<figcaption>` text content; any nested markup is flattened on round-trip. */
16
+ caption?: string | null;
17
+ captionPosition?: ImageRefCaptionPosition | null;
18
+ }
19
+ export interface ImageRefOptions {
20
+ HTMLAttributes: Record<string, unknown>;
21
+ }
22
+ declare module '@tiptap/core' {
23
+ interface Commands<ReturnType> {
24
+ imageRef: {
25
+ /** Insert an `image-ref` block. `imageId` is required. */
26
+ insertImageRef: (attributes: ImageRefAttributes) => ReturnType;
27
+ };
28
+ }
29
+ }
30
+ /**
31
+ * `image-ref` — a semantic, URL-free reference to a media asset.
32
+ *
33
+ * Renders a `<figure data-block="image-ref" data-image-id="…">` (with an optional
34
+ * `<figcaption>`), decoupling document content from the resolved image URL. The
35
+ * consumer hydrates the figure by attribute (`data-image-id`) at render time —
36
+ * the document itself never embeds a URL or any presentation. In the editor the
37
+ * node shows a placeholder (it has no URL to display); resolving and showing the
38
+ * real image is the consumer's / media-picker's job.
39
+ */
40
+ export declare const ImageRef: Node<ImageRefOptions, any>;
@@ -0,0 +1,138 @@
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,3 +1,4 @@
1
1
  export * from './heading.node';
2
2
  export * from './dynamic-field.node';
3
3
  export * from './page-break.node';
4
+ export * from './image-ref.node';
@@ -1,3 +1,4 @@
1
1
  export * from './heading.node';
2
2
  export * from './dynamic-field.node';
3
3
  export * from './page-break.node';
4
+ export * from './image-ref.node';
@@ -0,0 +1,10 @@
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,13 +0,0 @@
1
- import { Editor } from '@tiptap/core';
2
- import type { Extensions } from '@tiptap/core';
3
- /**
4
- * Create a test editor instance with minimal required extensions
5
- * @param extensions Additional extensions to include
6
- * @param content Initial content (HTML string or JSON)
7
- * @returns Editor instance
8
- */
9
- export declare function createTestEditor(extensions?: Extensions, content?: string | Record<string, unknown>): Editor;
10
- /**
11
- * Destroy editor and cleanup
12
- */
13
- export declare function destroyEditor(editor: Editor): void;
@@ -1,25 +0,0 @@
1
- import { Editor } from '@tiptap/core';
2
- import { Document } from '@tiptap/extension-document';
3
- import { Paragraph } from '@tiptap/extension-paragraph';
4
- import { Text } from '@tiptap/extension-text';
5
- /**
6
- * Create a test editor instance with minimal required extensions
7
- * @param extensions Additional extensions to include
8
- * @param content Initial content (HTML string or JSON)
9
- * @returns Editor instance
10
- */ export function createTestEditor(extensions = [], content = '') {
11
- return new Editor({
12
- extensions: [
13
- Document,
14
- Paragraph,
15
- Text,
16
- ...extensions
17
- ],
18
- content
19
- });
20
- }
21
- /**
22
- * Destroy editor and cleanup
23
- */ export function destroyEditor(editor) {
24
- editor.destroy();
25
- }