@uniweb/content-reader 1.0.1 → 1.0.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/README.md +131 -25
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/parser/attributes.js +144 -0
- package/src/parser/block.js +1 -1
- package/src/parser/inline.js +71 -6
- package/src/parser/marked-extensions.js +117 -0
- package/src/parser/tables.js +21 -18
- package/src/schema/index.js +34 -2
- package/tests/parser.test.js +269 -40
package/README.md
CHANGED
|
@@ -17,25 +17,116 @@ A JavaScript library for converting Markdown content into ProseMirror-compatible
|
|
|
17
17
|
|
|
18
18
|
### Extended Syntax
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
#### Curly Brace Attributes
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-

|
|
24
|
-

|
|
25
|
-
```
|
|
22
|
+
Add rich attributes to images and links using `{...}` syntax:
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
```markdown
|
|
25
|
+
{role=hero width=800 loading=lazy}
|
|
26
|
+
[Link](https://example.com){target=_blank rel=noopener}
|
|
27
|
+
[Button](https://example.com){.button variant=secondary icon=arrow}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Supported attribute formats:**
|
|
31
|
+
- `key=value` - Standard attribute
|
|
32
|
+
- `key="value with spaces"` - Quoted value
|
|
33
|
+
- `.className` - CSS class (multiple allowed)
|
|
34
|
+
- `#idName` - Element ID
|
|
35
|
+
- `booleanAttr` - Boolean attribute (sets to `true`)
|
|
36
|
+
|
|
37
|
+
#### Image Attributes
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
# Basic image with role and dimensions
|
|
41
|
+
{role=hero width=1200 height=600}
|
|
42
|
+
|
|
43
|
+
# Video with poster and playback options
|
|
44
|
+
{role=video poster=./poster.jpg autoplay muted loop}
|
|
45
|
+
|
|
46
|
+
# PDF with preview thumbnail
|
|
47
|
+
{role=pdf preview=./preview.jpg}
|
|
48
|
+
|
|
49
|
+
# Styling attributes
|
|
50
|
+
{fit=cover position=center loading=lazy}
|
|
51
|
+
|
|
52
|
+
# Classes and IDs
|
|
53
|
+
{.featured .rounded #main-logo}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Attribute | Description |
|
|
57
|
+
|-----------|-------------|
|
|
58
|
+
| `role` | Semantic role: `image`, `icon`, `hero`, `video`, `pdf`, etc. |
|
|
59
|
+
| `width`, `height` | Dimensions (pixels) |
|
|
60
|
+
| `loading` | Loading behavior: `lazy`, `eager` |
|
|
61
|
+
| `poster` | Poster image for videos |
|
|
62
|
+
| `preview` | Preview image for PDFs/documents |
|
|
63
|
+
| `autoplay`, `muted`, `loop`, `controls` | Video playback options |
|
|
64
|
+
| `fit` | Object-fit: `cover`, `contain`, `fill`, etc. |
|
|
65
|
+
| `position` | Object-position value |
|
|
66
|
+
| `.class`, `#id` | CSS class and ID |
|
|
67
|
+
|
|
68
|
+
#### Link Attributes
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
# External link with target
|
|
72
|
+
[External Link](https://example.com){target=_blank rel="noopener noreferrer"}
|
|
73
|
+
|
|
74
|
+
# Download link
|
|
75
|
+
[Download PDF](./document.pdf){download}
|
|
76
|
+
|
|
77
|
+
# Link with custom filename for download
|
|
78
|
+
[Get Report](./data.pdf){download="annual-report.pdf"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Attribute | Description |
|
|
82
|
+
|-----------|-------------|
|
|
83
|
+
| `target` | Link target: `_blank`, `_self`, etc. |
|
|
84
|
+
| `rel` | Link relationship: `noopener`, `noreferrer`, etc. |
|
|
85
|
+
| `download` | Download attribute (boolean or filename) |
|
|
86
|
+
| `.class` | CSS class |
|
|
87
|
+
|
|
88
|
+
#### Button Attributes
|
|
89
|
+
|
|
90
|
+
Buttons can be created using the `.button` class or the legacy `button:` prefix:
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
# Using .button class (recommended)
|
|
94
|
+
[Get Started](https://example.com){.button variant=primary size=lg}
|
|
95
|
+
[Learn More](https://example.com){.button variant=secondary icon=arrow-right}
|
|
96
|
+
|
|
97
|
+
# Legacy prefix syntax (still supported)
|
|
98
|
+
[Button Text](button:https://example.com)
|
|
99
|
+
```
|
|
28
100
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
101
|
+
| Attribute | Description |
|
|
102
|
+
|-----------|-------------|
|
|
103
|
+
| `variant` | Style variant: `primary`, `secondary`, `outline`, `ghost` |
|
|
104
|
+
| `size` | Button size: `sm`, `md`, `lg` |
|
|
105
|
+
| `icon` | Icon name or path |
|
|
106
|
+
| `target`, `rel`, `download` | Same as links |
|
|
32
107
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
108
|
+
#### Legacy Prefix Syntax
|
|
109
|
+
|
|
110
|
+
The original prefix syntax is still supported for backward compatibility:
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
# Image with role prefix
|
|
114
|
+

|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
# Button with prefix
|
|
118
|
+
[Button Text](button:https://example.com)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Tables with Alignment
|
|
122
|
+
|
|
123
|
+
Full support for aligned columns:
|
|
124
|
+
|
|
125
|
+
```markdown
|
|
126
|
+
| Left | Center | Right |
|
|
127
|
+
| :--- | :----: | ----: |
|
|
128
|
+
| Text | Text | Text |
|
|
129
|
+
```
|
|
39
130
|
|
|
40
131
|
### Developer-Friendly Features
|
|
41
132
|
|
|
@@ -48,7 +139,7 @@ A JavaScript library for converting Markdown content into ProseMirror-compatible
|
|
|
48
139
|
## Installation
|
|
49
140
|
|
|
50
141
|
```bash
|
|
51
|
-
npm install @
|
|
142
|
+
npm install @uniweb/content-reader
|
|
52
143
|
```
|
|
53
144
|
|
|
54
145
|
## Usage
|
|
@@ -56,7 +147,7 @@ npm install @uniwebcms/content-reader
|
|
|
56
147
|
Basic usage:
|
|
57
148
|
|
|
58
149
|
```javascript
|
|
59
|
-
const { markdownToProseMirror } = require("@
|
|
150
|
+
const { markdownToProseMirror } = require("@uniweb/content-reader");
|
|
60
151
|
|
|
61
152
|
const markdown = `
|
|
62
153
|
# Hello World
|
|
@@ -77,7 +168,7 @@ The library is designed to work seamlessly with TipTap editors:
|
|
|
77
168
|
|
|
78
169
|
```javascript
|
|
79
170
|
import { Editor } from "@tiptap/core";
|
|
80
|
-
import { markdownToProseMirror } from "@
|
|
171
|
+
import { markdownToProseMirror } from "@uniweb/content-reader";
|
|
81
172
|
|
|
82
173
|
const editor = new Editor({
|
|
83
174
|
content: markdownToProseMirror(markdown),
|
|
@@ -87,19 +178,34 @@ const editor = new Editor({
|
|
|
87
178
|
|
|
88
179
|
### Advanced Features
|
|
89
180
|
|
|
90
|
-
#### Working with
|
|
181
|
+
#### Working with Rich Media
|
|
182
|
+
|
|
183
|
+
The library supports extended syntax for images, videos, and documents:
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
const markdown = `
|
|
187
|
+
{role=hero width=1200 fit=cover}
|
|
188
|
+
{role=video poster=./poster.jpg autoplay muted}
|
|
189
|
+
{role=pdf preview=./preview.jpg}
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const doc = markdownToProseMirror(markdown);
|
|
193
|
+
// Each media element will have rich attributes for component rendering
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Working with Buttons and Links
|
|
91
197
|
|
|
92
|
-
|
|
198
|
+
Create styled buttons and links with attributes:
|
|
93
199
|
|
|
94
200
|
```javascript
|
|
95
201
|
const markdown = `
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
202
|
+
[Get Started](https://example.com){.button variant=primary size=lg}
|
|
203
|
+
[Download](./file.pdf){download}
|
|
204
|
+
[External](https://example.com){target=_blank rel=noopener}
|
|
99
205
|
`;
|
|
100
206
|
|
|
101
207
|
const doc = markdownToProseMirror(markdown);
|
|
102
|
-
//
|
|
208
|
+
// Links and buttons will have appropriate attributes for rendering
|
|
103
209
|
```
|
|
104
210
|
|
|
105
211
|
#### Handling Tables with Alignment
|
|
@@ -137,7 +243,7 @@ We welcome contributions! Please see our contributing guidelines for details.
|
|
|
137
243
|
1. Clone the repository:
|
|
138
244
|
|
|
139
245
|
```bash
|
|
140
|
-
git clone https://github.com/
|
|
246
|
+
git clone https://github.com/uniweb/content-reader.git
|
|
141
247
|
```
|
|
142
248
|
|
|
143
249
|
2. Install dependencies:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -7,6 +7,10 @@ import { marked } from "marked";
|
|
|
7
7
|
import { parseMarkdownContent } from "./parser/index.js";
|
|
8
8
|
import { getBaseSchema } from "./schema/index.js";
|
|
9
9
|
import { isValidUniwebMarkdown } from "./utils.js";
|
|
10
|
+
import { getMarkedExtensions } from "./parser/marked-extensions.js";
|
|
11
|
+
|
|
12
|
+
// Configure marked with our custom extensions for attribute syntax
|
|
13
|
+
marked.use(getMarkedExtensions());
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Convert markdown content to ProseMirror document structure
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Parse curly brace attributes from markdown
|
|
3
|
+
*
|
|
4
|
+
* Supports syntax like: {role=hero width=1200 .class #id autoplay}
|
|
5
|
+
*
|
|
6
|
+
* Attribute types:
|
|
7
|
+
* - key=value → { key: "value" }
|
|
8
|
+
* - key="value" → { key: "value" } (quoted, allows spaces)
|
|
9
|
+
* - key='value' → { key: "value" } (single quoted)
|
|
10
|
+
* - .className → added to classes array
|
|
11
|
+
* - #idName → { id: "idName" }
|
|
12
|
+
* - booleanKey → { booleanKey: true }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse an attribute string like "role=hero width=1200 .featured autoplay"
|
|
17
|
+
*
|
|
18
|
+
* @param {string} attrString - The attribute string (without curly braces)
|
|
19
|
+
* @returns {Object} Parsed attributes object
|
|
20
|
+
*/
|
|
21
|
+
export function parseAttributeString(attrString) {
|
|
22
|
+
if (!attrString || typeof attrString !== 'string') {
|
|
23
|
+
return {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const attrs = {}
|
|
27
|
+
const classes = []
|
|
28
|
+
|
|
29
|
+
// Regex to match different attribute patterns
|
|
30
|
+
// Handles: key="value", key='value', key=value, .class, #id, boolean
|
|
31
|
+
const pattern = /(?:([a-zA-Z_][\w-]*)=(?:"([^"]*)"|'([^']*)'|([^\s}]+))|\.([a-zA-Z_][\w-]*)|#([a-zA-Z_][\w-]*)|([a-zA-Z_][\w-]*)(?=\s|$))/g
|
|
32
|
+
|
|
33
|
+
let match
|
|
34
|
+
while ((match = pattern.exec(attrString)) !== null) {
|
|
35
|
+
const [, key, quotedDouble, quotedSingle, unquoted, className, idName, booleanKey] = match
|
|
36
|
+
|
|
37
|
+
if (key) {
|
|
38
|
+
// key=value attribute
|
|
39
|
+
attrs[key] = quotedDouble ?? quotedSingle ?? unquoted
|
|
40
|
+
} else if (className) {
|
|
41
|
+
// .className
|
|
42
|
+
classes.push(className)
|
|
43
|
+
} else if (idName) {
|
|
44
|
+
// #id
|
|
45
|
+
attrs.id = idName
|
|
46
|
+
} else if (booleanKey) {
|
|
47
|
+
// boolean attribute (no value)
|
|
48
|
+
attrs[booleanKey] = true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add classes array if any were found
|
|
53
|
+
if (classes.length > 0) {
|
|
54
|
+
attrs.class = classes.join(' ')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return attrs
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract attributes block from the end of a string
|
|
62
|
+
*
|
|
63
|
+
* @param {string} text - Text that may end with {attributes}
|
|
64
|
+
* @returns {{ text: string, attrs: Object }} The text without attrs and parsed attrs
|
|
65
|
+
*/
|
|
66
|
+
export function extractTrailingAttributes(text) {
|
|
67
|
+
if (!text || typeof text !== 'string') {
|
|
68
|
+
return { text: text || '', attrs: {} }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Match {attributes} at the end of the string
|
|
72
|
+
// Handles nested braces in values by matching balanced braces
|
|
73
|
+
const match = text.match(/\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\s*$/)
|
|
74
|
+
|
|
75
|
+
if (!match) {
|
|
76
|
+
return { text, attrs: {} }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const attrString = match[1]
|
|
80
|
+
const textWithoutAttrs = text.slice(0, match.index).trimEnd()
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
text: textWithoutAttrs,
|
|
84
|
+
attrs: parseAttributeString(attrString)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a string contains an attribute block
|
|
90
|
+
*
|
|
91
|
+
* @param {string} text - Text to check
|
|
92
|
+
* @returns {boolean} True if text contains {attributes}
|
|
93
|
+
*/
|
|
94
|
+
export function hasAttributes(text) {
|
|
95
|
+
return /\{[^{}]+\}\s*$/.test(text)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Merge multiple attribute objects, with later objects taking precedence
|
|
100
|
+
*
|
|
101
|
+
* @param {...Object} attrObjects - Attribute objects to merge
|
|
102
|
+
* @returns {Object} Merged attributes
|
|
103
|
+
*/
|
|
104
|
+
export function mergeAttributes(...attrObjects) {
|
|
105
|
+
const result = {}
|
|
106
|
+
const allClasses = []
|
|
107
|
+
|
|
108
|
+
for (const attrs of attrObjects) {
|
|
109
|
+
if (!attrs) continue
|
|
110
|
+
|
|
111
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
112
|
+
if (key === 'class') {
|
|
113
|
+
// Accumulate classes
|
|
114
|
+
allClasses.push(value)
|
|
115
|
+
} else {
|
|
116
|
+
result[key] = value
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (allClasses.length > 0) {
|
|
122
|
+
result.class = allClasses.join(' ')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Normalize attribute names to camelCase
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} attrs - Attributes with potentially kebab-case keys
|
|
132
|
+
* @returns {Object} Attributes with camelCase keys
|
|
133
|
+
*/
|
|
134
|
+
export function normalizeAttributeNames(attrs) {
|
|
135
|
+
const result = {}
|
|
136
|
+
|
|
137
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
138
|
+
// Convert kebab-case to camelCase
|
|
139
|
+
const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
140
|
+
result[camelKey] = value
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
}
|
package/src/parser/block.js
CHANGED
package/src/parser/inline.js
CHANGED
|
@@ -63,8 +63,29 @@ function parseInline(token, schema, removeNewLine = false) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (token.type === "link") {
|
|
66
|
-
|
|
67
|
-
const
|
|
66
|
+
// Check for button: prefix or .button class in attrs
|
|
67
|
+
const hasButtonPrefix = token.href.startsWith("button:");
|
|
68
|
+
const hasButtonClass = token.attrs?.class?.includes("button");
|
|
69
|
+
const isButton = hasButtonPrefix || hasButtonClass;
|
|
70
|
+
|
|
71
|
+
const href = hasButtonPrefix ? token.href.substring(7) : token.href;
|
|
72
|
+
|
|
73
|
+
// Extract known link/button attributes from curly brace attrs
|
|
74
|
+
const {
|
|
75
|
+
variant = "primary",
|
|
76
|
+
download,
|
|
77
|
+
target,
|
|
78
|
+
rel,
|
|
79
|
+
size,
|
|
80
|
+
icon,
|
|
81
|
+
...otherAttrs
|
|
82
|
+
} = token.attrs || {};
|
|
83
|
+
|
|
84
|
+
// Remove 'button' from class if present (it's used as a type indicator)
|
|
85
|
+
let className = otherAttrs.class;
|
|
86
|
+
if (className) {
|
|
87
|
+
className = className.replace(/\bbutton\b/, "").trim() || undefined;
|
|
88
|
+
}
|
|
68
89
|
|
|
69
90
|
return [
|
|
70
91
|
{
|
|
@@ -75,7 +96,13 @@ function parseInline(token, schema, removeNewLine = false) {
|
|
|
75
96
|
attrs: {
|
|
76
97
|
href,
|
|
77
98
|
title: token.title || null,
|
|
78
|
-
...(isButton && { variant
|
|
99
|
+
...(isButton && { variant }),
|
|
100
|
+
...(download !== undefined && { download }),
|
|
101
|
+
...(target && { target }),
|
|
102
|
+
...(rel && { rel }),
|
|
103
|
+
...(size && { size }),
|
|
104
|
+
...(icon && { icon }),
|
|
105
|
+
...(className && { class: className }),
|
|
79
106
|
},
|
|
80
107
|
},
|
|
81
108
|
],
|
|
@@ -87,16 +114,35 @@ function parseInline(token, schema, removeNewLine = false) {
|
|
|
87
114
|
if (token.type === "image") {
|
|
88
115
|
let role, src;
|
|
89
116
|
|
|
90
|
-
// Find the first colon to handle role:url format correctly
|
|
117
|
+
// Find the first colon to handle role:url format correctly (legacy syntax)
|
|
91
118
|
if (token.href.includes(":") && !token.href.startsWith("http")) {
|
|
92
119
|
const colonIndex = token.href.indexOf(":");
|
|
93
120
|
role = token.href.substring(0, colonIndex);
|
|
94
121
|
src = token.href.substring(colonIndex + 1);
|
|
95
122
|
} else {
|
|
96
|
-
role = "image";
|
|
97
123
|
src = token.href;
|
|
98
124
|
}
|
|
99
125
|
|
|
126
|
+
// Extract known image attributes from curly brace attrs
|
|
127
|
+
const {
|
|
128
|
+
role: attrRole,
|
|
129
|
+
width,
|
|
130
|
+
height,
|
|
131
|
+
loading,
|
|
132
|
+
poster, // For videos: explicit poster image
|
|
133
|
+
preview, // For PDFs/documents: preview image
|
|
134
|
+
autoplay,
|
|
135
|
+
muted,
|
|
136
|
+
loop,
|
|
137
|
+
controls,
|
|
138
|
+
fit, // object-fit: cover, contain, etc.
|
|
139
|
+
position, // object-position
|
|
140
|
+
...otherAttrs
|
|
141
|
+
} = token.attrs || {};
|
|
142
|
+
|
|
143
|
+
// Attribute role takes precedence over prefix role
|
|
144
|
+
const finalRole = attrRole || role || "image";
|
|
145
|
+
|
|
100
146
|
return [
|
|
101
147
|
{
|
|
102
148
|
type: "image",
|
|
@@ -104,7 +150,26 @@ function parseInline(token, schema, removeNewLine = false) {
|
|
|
104
150
|
src,
|
|
105
151
|
caption: token.title || null,
|
|
106
152
|
alt: text || null,
|
|
107
|
-
role,
|
|
153
|
+
role: finalRole,
|
|
154
|
+
// Dimension attributes
|
|
155
|
+
...(width && { width: parseInt(width, 10) || width }),
|
|
156
|
+
...(height && { height: parseInt(height, 10) || height }),
|
|
157
|
+
// Loading behavior
|
|
158
|
+
...(loading && { loading }),
|
|
159
|
+
// Media attributes (for video/document roles)
|
|
160
|
+
...(poster && { poster }),
|
|
161
|
+
...(preview && { preview }),
|
|
162
|
+
// Video-specific attributes
|
|
163
|
+
...(autoplay !== undefined && { autoplay }),
|
|
164
|
+
...(muted !== undefined && { muted }),
|
|
165
|
+
...(loop !== undefined && { loop }),
|
|
166
|
+
...(controls !== undefined && { controls }),
|
|
167
|
+
// Styling attributes
|
|
168
|
+
...(fit && { fit }),
|
|
169
|
+
...(position && { position }),
|
|
170
|
+
// Any other custom attributes
|
|
171
|
+
...(otherAttrs.class && { class: otherAttrs.class }),
|
|
172
|
+
...(otherAttrs.id && { id: otherAttrs.id }),
|
|
108
173
|
},
|
|
109
174
|
},
|
|
110
175
|
];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Marked extensions for enhanced markdown syntax
|
|
3
|
+
*
|
|
4
|
+
* Adds support for curly brace attributes on images and links:
|
|
5
|
+
* - {attrs}
|
|
6
|
+
* - [text](href){attrs}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { parseAttributeString } from './attributes.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regex patterns for matching markdown elements with optional attributes
|
|
13
|
+
*/
|
|
14
|
+
const PATTERNS = {
|
|
15
|
+
// Image: {attrs}
|
|
16
|
+
// Captures: alt, src, title (optional), attrs (optional)
|
|
17
|
+
image: /^!\[([^\]]*)\]\(([^)"'\s]+)(?:\s+["']([^"']*)["'])?\)(?:\{([^}]*)\})?/,
|
|
18
|
+
|
|
19
|
+
// Link: [text](href "title"){attrs}
|
|
20
|
+
// Captures: text, href, title (optional), attrs (optional)
|
|
21
|
+
link: /^\[([^\]]+)\]\(([^)"'\s]+)(?:\s+["']([^"']*)["'])?\)(?:\{([^}]*)\})?/,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a marked extension for images with attribute support
|
|
26
|
+
*
|
|
27
|
+
* @returns {Object} Marked tokenizer extension
|
|
28
|
+
*/
|
|
29
|
+
export function createImageExtension() {
|
|
30
|
+
return {
|
|
31
|
+
name: 'image',
|
|
32
|
+
level: 'inline',
|
|
33
|
+
start(src) {
|
|
34
|
+
return src.indexOf('![')
|
|
35
|
+
},
|
|
36
|
+
tokenizer(src) {
|
|
37
|
+
const match = PATTERNS.image.exec(src)
|
|
38
|
+
if (!match) return
|
|
39
|
+
|
|
40
|
+
const [raw, alt, href, title, attrString] = match
|
|
41
|
+
|
|
42
|
+
// Parse attributes from curly braces
|
|
43
|
+
const attrs = attrString ? parseAttributeString(attrString) : {}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
type: 'image',
|
|
47
|
+
raw,
|
|
48
|
+
text: alt || '',
|
|
49
|
+
href,
|
|
50
|
+
title: title || null,
|
|
51
|
+
attrs, // Extended: parsed attributes
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a marked extension for links with attribute support
|
|
59
|
+
*
|
|
60
|
+
* @returns {Object} Marked tokenizer extension
|
|
61
|
+
*/
|
|
62
|
+
export function createLinkExtension() {
|
|
63
|
+
return {
|
|
64
|
+
name: 'link',
|
|
65
|
+
level: 'inline',
|
|
66
|
+
start(src) {
|
|
67
|
+
// Don't match images (starting with !)
|
|
68
|
+
const idx = src.indexOf('[')
|
|
69
|
+
if (idx > 0 && src[idx - 1] === '!') {
|
|
70
|
+
// Find next [ that's not part of an image
|
|
71
|
+
const nextIdx = src.indexOf('[', idx + 1)
|
|
72
|
+
return nextIdx >= 0 ? nextIdx : -1
|
|
73
|
+
}
|
|
74
|
+
return idx
|
|
75
|
+
},
|
|
76
|
+
tokenizer(src) {
|
|
77
|
+
// Don't match if this is an image
|
|
78
|
+
if (src.startsWith('![')) return
|
|
79
|
+
|
|
80
|
+
const match = PATTERNS.link.exec(src)
|
|
81
|
+
if (!match) return
|
|
82
|
+
|
|
83
|
+
const [raw, text, href, title, attrString] = match
|
|
84
|
+
|
|
85
|
+
// Parse attributes from curly braces
|
|
86
|
+
const attrs = attrString ? parseAttributeString(attrString) : {}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
type: 'link',
|
|
90
|
+
raw,
|
|
91
|
+
text,
|
|
92
|
+
href,
|
|
93
|
+
title: title || null,
|
|
94
|
+
attrs, // Extended: parsed attributes
|
|
95
|
+
// Include tokens for nested content
|
|
96
|
+
tokens: [],
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
childTokens: ['tokens'],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get all custom marked extensions
|
|
105
|
+
*
|
|
106
|
+
* @returns {Object} Marked extensions configuration
|
|
107
|
+
*/
|
|
108
|
+
export function getMarkedExtensions() {
|
|
109
|
+
return {
|
|
110
|
+
extensions: [
|
|
111
|
+
createImageExtension(),
|
|
112
|
+
createLinkExtension(),
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default getMarkedExtensions
|
package/src/parser/tables.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* @fileoverview Parse markdown tables
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { marked } from "marked";
|
|
6
5
|
import { parseInline } from "./inline.js";
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -21,7 +20,7 @@ function getColumnAlignment(colDef) {
|
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* Parse table row content
|
|
24
|
-
* @param {
|
|
23
|
+
* @param {Array} row - Row cells (objects with { text, tokens } in marked v11+)
|
|
25
24
|
* @param {boolean} isHeader - Whether this is a header row
|
|
26
25
|
* @param {Array} alignments - Column alignments
|
|
27
26
|
* @param {Object} schema - ProseMirror schema
|
|
@@ -30,23 +29,27 @@ function getColumnAlignment(colDef) {
|
|
|
30
29
|
function parseTableRow(row, isHeader, alignments, schema) {
|
|
31
30
|
return {
|
|
32
31
|
type: "tableRow",
|
|
33
|
-
content: row.map((cell, index) =>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
parseInline(t, schema)
|
|
46
|
-
),
|
|
32
|
+
content: row.map((cell, index) => {
|
|
33
|
+
// In marked v11+, cells are objects with { text, tokens }
|
|
34
|
+
// Use the pre-parsed tokens array directly
|
|
35
|
+
const tokens = cell.tokens || [];
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
type: "tableCell",
|
|
39
|
+
attrs: {
|
|
40
|
+
colspan: 1,
|
|
41
|
+
rowspan: 1,
|
|
42
|
+
align: alignments[index] || null,
|
|
43
|
+
header: isHeader,
|
|
47
44
|
},
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "paragraph",
|
|
48
|
+
content: tokens.flatMap((t) => parseInline(t, schema)),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
55
|
|
package/src/schema/index.js
CHANGED
|
@@ -32,10 +32,30 @@ const baseNodes = {
|
|
|
32
32
|
|
|
33
33
|
image: {
|
|
34
34
|
attrs: {
|
|
35
|
+
// Core attributes
|
|
35
36
|
src: {},
|
|
36
37
|
caption: { default: null },
|
|
37
38
|
alt: { default: null },
|
|
38
|
-
role: { default: "
|
|
39
|
+
role: { default: "image" }, // image, icon, hero, video, pdf, etc.
|
|
40
|
+
// Dimension attributes
|
|
41
|
+
width: { default: null },
|
|
42
|
+
height: { default: null },
|
|
43
|
+
// Loading behavior
|
|
44
|
+
loading: { default: null }, // lazy, eager
|
|
45
|
+
// Media attributes (for video/document roles)
|
|
46
|
+
poster: { default: null }, // Explicit poster image for videos
|
|
47
|
+
preview: { default: null }, // Preview image for PDFs/documents
|
|
48
|
+
// Video-specific attributes
|
|
49
|
+
autoplay: { default: null },
|
|
50
|
+
muted: { default: null },
|
|
51
|
+
loop: { default: null },
|
|
52
|
+
controls: { default: null },
|
|
53
|
+
// Styling attributes
|
|
54
|
+
fit: { default: null }, // object-fit: cover, contain, fill, etc.
|
|
55
|
+
position: { default: null }, // object-position
|
|
56
|
+
// Generic attributes
|
|
57
|
+
class: { default: null },
|
|
58
|
+
id: { default: null },
|
|
39
59
|
},
|
|
40
60
|
// group: "block inline",
|
|
41
61
|
},
|
|
@@ -114,13 +134,25 @@ const baseMarks = {
|
|
|
114
134
|
attrs: {
|
|
115
135
|
href: {},
|
|
116
136
|
title: { default: null },
|
|
137
|
+
// Extended attributes
|
|
138
|
+
target: { default: null }, // _blank, _self, etc.
|
|
139
|
+
rel: { default: null }, // noopener, noreferrer, etc.
|
|
140
|
+
download: { default: null }, // Download attribute (true or filename)
|
|
141
|
+
class: { default: null },
|
|
117
142
|
},
|
|
118
143
|
},
|
|
119
144
|
button: {
|
|
120
145
|
attrs: {
|
|
121
146
|
href: {},
|
|
122
147
|
title: { default: null },
|
|
123
|
-
variant: { default: "primary" },
|
|
148
|
+
variant: { default: "primary" }, // primary, secondary, outline, ghost, etc.
|
|
149
|
+
// Extended attributes
|
|
150
|
+
size: { default: null }, // sm, md, lg
|
|
151
|
+
icon: { default: null }, // Icon name or path
|
|
152
|
+
target: { default: null },
|
|
153
|
+
rel: { default: null },
|
|
154
|
+
download: { default: null },
|
|
155
|
+
class: { default: null },
|
|
124
156
|
},
|
|
125
157
|
},
|
|
126
158
|
code: {
|
package/tests/parser.test.js
CHANGED
|
@@ -179,75 +179,63 @@ describe("Basic Markdown Parsing", () => {
|
|
|
179
179
|
|
|
180
180
|
describe("Extended Syntax", () => {
|
|
181
181
|
test("parses images without role", () => {
|
|
182
|
-
|
|
182
|
+
// Images are extracted from paragraphs to root level for component rendering
|
|
183
|
+
const markdown = "";
|
|
183
184
|
const result = markdownToProseMirror(markdown);
|
|
184
185
|
|
|
185
186
|
expect(result).toEqual({
|
|
186
187
|
type: "doc",
|
|
187
188
|
content: [
|
|
188
189
|
{
|
|
189
|
-
type: "
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
alt: null,
|
|
197
|
-
role: "image",
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
],
|
|
190
|
+
type: "image",
|
|
191
|
+
attrs: {
|
|
192
|
+
src: "path/to/image.svg",
|
|
193
|
+
caption: null,
|
|
194
|
+
alt: "Alt Text",
|
|
195
|
+
role: "image",
|
|
196
|
+
},
|
|
201
197
|
},
|
|
202
198
|
],
|
|
203
199
|
});
|
|
204
200
|
});
|
|
205
201
|
|
|
206
202
|
test("parses images with roles", () => {
|
|
207
|
-
|
|
203
|
+
// Images are extracted from paragraphs to root level for component rendering
|
|
204
|
+
const markdown = '';
|
|
208
205
|
const result = markdownToProseMirror(markdown);
|
|
209
206
|
|
|
210
207
|
expect(result).toEqual({
|
|
211
208
|
type: "doc",
|
|
212
209
|
content: [
|
|
213
210
|
{
|
|
214
|
-
type: "
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
alt: "Alt text",
|
|
222
|
-
role: "icon",
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
],
|
|
211
|
+
type: "image",
|
|
212
|
+
attrs: {
|
|
213
|
+
src: "path/to/image.svg",
|
|
214
|
+
caption: "Caption text",
|
|
215
|
+
alt: "Alt Text",
|
|
216
|
+
role: "icon",
|
|
217
|
+
},
|
|
226
218
|
},
|
|
227
219
|
],
|
|
228
220
|
});
|
|
229
221
|
});
|
|
230
222
|
|
|
231
|
-
test("parses images
|
|
232
|
-
|
|
223
|
+
test("parses images with URL", () => {
|
|
224
|
+
// Images are extracted from paragraphs to root level for component rendering
|
|
225
|
+
const markdown = "";
|
|
233
226
|
const result = markdownToProseMirror(markdown);
|
|
234
227
|
|
|
235
228
|
expect(result).toEqual({
|
|
236
229
|
type: "doc",
|
|
237
230
|
content: [
|
|
238
231
|
{
|
|
239
|
-
type: "
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
alt: null,
|
|
247
|
-
role: "image",
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
],
|
|
232
|
+
type: "image",
|
|
233
|
+
attrs: {
|
|
234
|
+
src: "https://test.com",
|
|
235
|
+
caption: null,
|
|
236
|
+
alt: "Alt Text",
|
|
237
|
+
role: "image",
|
|
238
|
+
},
|
|
251
239
|
},
|
|
252
240
|
],
|
|
253
241
|
});
|
|
@@ -341,3 +329,244 @@ describe("Extended Syntax", () => {
|
|
|
341
329
|
});
|
|
342
330
|
});
|
|
343
331
|
});
|
|
332
|
+
|
|
333
|
+
describe("Curly Brace Attributes", () => {
|
|
334
|
+
test("parses image with role attribute", () => {
|
|
335
|
+
const markdown = "{role=hero}";
|
|
336
|
+
const result = markdownToProseMirror(markdown);
|
|
337
|
+
|
|
338
|
+
expect(result.content[0]).toEqual({
|
|
339
|
+
type: "image",
|
|
340
|
+
attrs: expect.objectContaining({
|
|
341
|
+
src: "./hero.jpg",
|
|
342
|
+
alt: "Hero Image",
|
|
343
|
+
role: "hero",
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("parses image with multiple attributes", () => {
|
|
349
|
+
const markdown = '{width=800 height=600 loading=lazy}';
|
|
350
|
+
const result = markdownToProseMirror(markdown);
|
|
351
|
+
|
|
352
|
+
expect(result.content[0]).toEqual({
|
|
353
|
+
type: "image",
|
|
354
|
+
attrs: expect.objectContaining({
|
|
355
|
+
src: "./photo.jpg",
|
|
356
|
+
alt: "Photo",
|
|
357
|
+
caption: "A beautiful photo",
|
|
358
|
+
width: 800,
|
|
359
|
+
height: 600,
|
|
360
|
+
loading: "lazy",
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("parses video with poster attribute", () => {
|
|
366
|
+
const markdown = "{role=video poster=./poster.jpg autoplay muted loop}";
|
|
367
|
+
const result = markdownToProseMirror(markdown);
|
|
368
|
+
|
|
369
|
+
expect(result.content[0]).toEqual({
|
|
370
|
+
type: "image",
|
|
371
|
+
attrs: expect.objectContaining({
|
|
372
|
+
src: "./intro.mp4",
|
|
373
|
+
role: "video",
|
|
374
|
+
poster: "./poster.jpg",
|
|
375
|
+
autoplay: true,
|
|
376
|
+
muted: true,
|
|
377
|
+
loop: true,
|
|
378
|
+
}),
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("parses PDF with preview attribute", () => {
|
|
383
|
+
const markdown = "{role=pdf preview=./guide-preview.jpg}";
|
|
384
|
+
const result = markdownToProseMirror(markdown);
|
|
385
|
+
|
|
386
|
+
expect(result.content[0]).toEqual({
|
|
387
|
+
type: "image",
|
|
388
|
+
attrs: expect.objectContaining({
|
|
389
|
+
src: "./guide.pdf",
|
|
390
|
+
role: "pdf",
|
|
391
|
+
preview: "./guide-preview.jpg",
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("parses image with class and id", () => {
|
|
397
|
+
const markdown = "{.featured #main-logo}";
|
|
398
|
+
const result = markdownToProseMirror(markdown);
|
|
399
|
+
|
|
400
|
+
expect(result.content[0]).toEqual({
|
|
401
|
+
type: "image",
|
|
402
|
+
attrs: expect.objectContaining({
|
|
403
|
+
src: "./logo.svg",
|
|
404
|
+
class: "featured",
|
|
405
|
+
id: "main-logo",
|
|
406
|
+
}),
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("parses link with target attribute", () => {
|
|
411
|
+
const markdown = '[External Link](https://example.com){target=_blank rel="noopener noreferrer"}';
|
|
412
|
+
const result = markdownToProseMirror(markdown);
|
|
413
|
+
|
|
414
|
+
expect(result.content[0].content[0]).toEqual({
|
|
415
|
+
type: "text",
|
|
416
|
+
text: "External Link",
|
|
417
|
+
marks: [
|
|
418
|
+
{
|
|
419
|
+
type: "link",
|
|
420
|
+
attrs: expect.objectContaining({
|
|
421
|
+
href: "https://example.com",
|
|
422
|
+
target: "_blank",
|
|
423
|
+
rel: "noopener noreferrer",
|
|
424
|
+
}),
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("parses download link", () => {
|
|
431
|
+
const markdown = "[Download PDF](./document.pdf){download}";
|
|
432
|
+
const result = markdownToProseMirror(markdown);
|
|
433
|
+
|
|
434
|
+
expect(result.content[0].content[0]).toEqual({
|
|
435
|
+
type: "text",
|
|
436
|
+
text: "Download PDF",
|
|
437
|
+
marks: [
|
|
438
|
+
{
|
|
439
|
+
type: "link",
|
|
440
|
+
attrs: expect.objectContaining({
|
|
441
|
+
href: "./document.pdf",
|
|
442
|
+
download: true,
|
|
443
|
+
}),
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("parses button with .button class", () => {
|
|
450
|
+
const markdown = "[Get Started](https://example.com){.button variant=secondary size=lg}";
|
|
451
|
+
const result = markdownToProseMirror(markdown);
|
|
452
|
+
|
|
453
|
+
expect(result.content[0].content[0]).toEqual({
|
|
454
|
+
type: "text",
|
|
455
|
+
text: "Get Started",
|
|
456
|
+
marks: [
|
|
457
|
+
{
|
|
458
|
+
type: "button",
|
|
459
|
+
attrs: expect.objectContaining({
|
|
460
|
+
href: "https://example.com",
|
|
461
|
+
variant: "secondary",
|
|
462
|
+
size: "lg",
|
|
463
|
+
}),
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("parses button with icon", () => {
|
|
470
|
+
const markdown = "[Learn More](https://example.com){.button icon=arrow-right}";
|
|
471
|
+
const result = markdownToProseMirror(markdown);
|
|
472
|
+
|
|
473
|
+
expect(result.content[0].content[0]).toEqual({
|
|
474
|
+
type: "text",
|
|
475
|
+
text: "Learn More",
|
|
476
|
+
marks: [
|
|
477
|
+
{
|
|
478
|
+
type: "button",
|
|
479
|
+
attrs: expect.objectContaining({
|
|
480
|
+
href: "https://example.com",
|
|
481
|
+
icon: "arrow-right",
|
|
482
|
+
}),
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("attribute role overrides prefix role (legacy compatibility)", () => {
|
|
489
|
+
// If both prefix and attribute role are present, attribute takes precedence
|
|
490
|
+
const markdown = "{role=hero}";
|
|
491
|
+
const result = markdownToProseMirror(markdown);
|
|
492
|
+
|
|
493
|
+
expect(result.content[0].attrs.role).toBe("hero");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("parses image with fit and position attributes", () => {
|
|
497
|
+
const markdown = "{fit=cover position=center}";
|
|
498
|
+
const result = markdownToProseMirror(markdown);
|
|
499
|
+
|
|
500
|
+
expect(result.content[0]).toEqual({
|
|
501
|
+
type: "image",
|
|
502
|
+
attrs: expect.objectContaining({
|
|
503
|
+
src: "./bg.jpg",
|
|
504
|
+
fit: "cover",
|
|
505
|
+
position: "center",
|
|
506
|
+
}),
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("parses video with controls attribute", () => {
|
|
511
|
+
const markdown = "{role=video controls muted}";
|
|
512
|
+
const result = markdownToProseMirror(markdown);
|
|
513
|
+
|
|
514
|
+
expect(result.content[0]).toEqual({
|
|
515
|
+
type: "image",
|
|
516
|
+
attrs: expect.objectContaining({
|
|
517
|
+
src: "./demo.mp4",
|
|
518
|
+
role: "video",
|
|
519
|
+
controls: true,
|
|
520
|
+
muted: true,
|
|
521
|
+
}),
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("parses download link with custom filename", () => {
|
|
526
|
+
const markdown = '[Get Report](./data.pdf){download="annual-report.pdf"}';
|
|
527
|
+
const result = markdownToProseMirror(markdown);
|
|
528
|
+
|
|
529
|
+
expect(result.content[0].content[0]).toEqual({
|
|
530
|
+
type: "text",
|
|
531
|
+
text: "Get Report",
|
|
532
|
+
marks: [
|
|
533
|
+
{
|
|
534
|
+
type: "link",
|
|
535
|
+
attrs: expect.objectContaining({
|
|
536
|
+
href: "./data.pdf",
|
|
537
|
+
download: "annual-report.pdf",
|
|
538
|
+
}),
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("parses image with multiple classes", () => {
|
|
545
|
+
const markdown = "{.featured .rounded .shadow}";
|
|
546
|
+
const result = markdownToProseMirror(markdown);
|
|
547
|
+
|
|
548
|
+
expect(result.content[0]).toEqual({
|
|
549
|
+
type: "image",
|
|
550
|
+
attrs: expect.objectContaining({
|
|
551
|
+
src: "./photo.jpg",
|
|
552
|
+
class: "featured rounded shadow",
|
|
553
|
+
}),
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("parses booleans in different positions", () => {
|
|
558
|
+
const markdown = "{muted role=video autoplay loop}";
|
|
559
|
+
const result = markdownToProseMirror(markdown);
|
|
560
|
+
|
|
561
|
+
expect(result.content[0]).toEqual({
|
|
562
|
+
type: "image",
|
|
563
|
+
attrs: expect.objectContaining({
|
|
564
|
+
src: "./clip.mp4",
|
|
565
|
+
role: "video",
|
|
566
|
+
muted: true,
|
|
567
|
+
autoplay: true,
|
|
568
|
+
loop: true,
|
|
569
|
+
}),
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
});
|