@lalalic/markcut 2.8.0 → 3.0.0
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/B] +2 -0
- package/README.md +29 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +12 -45
- package/skills/markcut/docs/components.md +89 -0
- package/skills/markcut/docs/markdown-descriptive.md +17 -2
- package/skills/markcut/docs/sound-effects.md +45 -0
- package/src/components/Markdown.tsx +138 -24
- package/src/components/Mermaid.tsx +223 -22
- package/src/config.mjs +2 -2
- package/src/context/EventContext.tsx +3 -0
- package/src/descriptive/compiler.ts +68 -29
- package/src/descriptive/markdown.ts +20 -0
- package/src/player/browser.tsx +95 -5
- package/src/player/bundle/player.js +1078 -629
- package/src/player/components/EditControls.tsx +6 -3
- package/src/player/components/EditMessagePanel.tsx +96 -0
- package/src/player/components/HeaderBar.tsx +9 -11
- package/src/player/components/index.ts +1 -0
- package/src/player/pipeline.mjs +72 -21
- package/src/player/server-shared.mjs +4 -1
- package/src/player/server.mjs +202 -42
- package/src/render/cli.mjs +1 -1
- package/src/schema/index.ts +4 -1
- package/src/types/Component.tsx +27 -1
- package/src/types/Effect.tsx +13 -6
- package/src/types/Folder.tsx +1 -1
- package/src/types/Map.tsx +51 -1
- package/src/utils/index.ts +14 -2
- package/tests/fixtures/md/animate-diagrams.md +40 -0
- package/tests/fixtures/md/electricity-grow.md +130 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/segments/test-clip_0to3_seg_1100to3000.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_0to3.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/.normalized/test-clip_audio.mp3 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/metadata.json +9 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.mp4 +0 -0
- package/tests/tmp/vision-1785081637127-video/videos/test-clip.vtt +5 -0
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +0 -8
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tmp/frontmatter-test.ts +0 -21
package/B]
ADDED
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ storyboard.md ──[parse]──▶ DescriptiveRoot ──[compile]──▶
|
|
|
48
48
|
| **Built-in Mermaid** | `<Mermaid source="..." theme="dark" />` renders Mermaid diagrams as inline SVG. No imports needed |
|
|
49
49
|
| **Styling** | Inline `style` strings on any node for CSS. JSX components use inline React styles |
|
|
50
50
|
| **Live edit** | `--edit` watches the input file, re-runs pipeline, auto-reloads player |
|
|
51
|
+
| **Player language** | Player UI supports English/Chinese. Use `?lang=en` or `?lang=zh` |
|
|
51
52
|
| **Storyboard** | `--storyboard` fast structure preview: replaces TTI/TTV prompts with placeholder components, skips slow generation. Implies `--edit` |
|
|
52
53
|
| **Label mode** | `--label` interactive player with per-scene label input, saves to labels.json |
|
|
53
54
|
| **CLI** | `render`, `preview` commands for MP4 export and Remotion Studio |
|
|
@@ -159,6 +160,34 @@ Compound variant labels (like `zh-tiktok`) are split on `-` to form a **variant
|
|
|
159
160
|
|
|
160
161
|
The browser player reads `window.VARIANT` from the URL path and fetches the correct compiled data via `/api/video-data?variant=<name>`. A **variant switcher bar** at the top of the player lets you jump between variants instantly.
|
|
161
162
|
|
|
163
|
+
### Player UI Language (EN / ZH)
|
|
164
|
+
|
|
165
|
+
The player UI (edit panel, placeholders, tooltips) supports English and Chinese.
|
|
166
|
+
|
|
167
|
+
- `?lang=en` for English
|
|
168
|
+
- `?lang=zh` for Chinese
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
|
|
172
|
+
- `http://localhost:3001?lang=en`
|
|
173
|
+
- `http://localhost:3001?lang=zh`
|
|
174
|
+
|
|
175
|
+
If `lang` is not provided, the player falls back to browser language (Chinese browsers default to `zh`, otherwise `en`).
|
|
176
|
+
|
|
177
|
+
### Map Language (EN / ZH)
|
|
178
|
+
|
|
179
|
+
`map` nodes support `language`/`lang` and `region` keys.
|
|
180
|
+
|
|
181
|
+
```md
|
|
182
|
+
- map duration:3 lang:zh region:CN waypoints:[31.23,121.47,"上海";39.90,116.40,"北京"]
|
|
183
|
+
- map duration:3 lang:en region:US waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Locale shorthand normalization:
|
|
187
|
+
|
|
188
|
+
- `lang:zh` / `language:zh` → `zh-CN` (default region `CN`)
|
|
189
|
+
- `lang:en` / `language:en` → `en` (default region `US`)
|
|
190
|
+
|
|
162
191
|
## `.markcut/` Directory Layout
|
|
163
192
|
|
|
164
193
|
When you run `preview` or `render`, all generated artifacts live under `.markcut/` next to the source file. With multiple variants, each gets its own subdirectory:
|
package/package.json
CHANGED
package/skills/markcut/SKILL.md
CHANGED
|
@@ -80,52 +80,15 @@ some common issues (photo or video can't be displayed, audio missing), take belo
|
|
|
80
80
|
| Topic | File |
|
|
81
81
|
|-------|------|
|
|
82
82
|
| Markdown descriptive format (primary authoring format) | [docs/markdown-descriptive.md](docs/markdown-descriptive.md) |
|
|
83
|
+
| Built-in components & common npm packages | [docs/components.md](docs/components.md) |
|
|
84
|
+
| Sound effects | [docs/sound-effects.md](docs/sound-effects.md) |
|
|
83
85
|
|
|
84
86
|
|
|
85
|
-
## Built-in Components (no imports needed)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
## Built-in Components
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
```md
|
|
91
|
-
- component jsx:"<Markdown source='# Hello\n\n**bold** text.' />"
|
|
92
|
-
```
|
|
93
|
-
- Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
|
|
94
|
-
- Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
|
|
95
|
-
- ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
|
|
96
|
-
- Importable: `import {Markdown} from "@lalalic/markcut/components"`
|
|
97
|
-
|
|
98
|
-
### `<Mermaid />` — render Mermaid diagrams as SVG
|
|
99
|
-
```md
|
|
100
|
-
- component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
|
|
101
|
-
```
|
|
102
|
-
- Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
|
|
103
|
-
- Uses `delayRender`/`continueRender` for async rendering — diagram is ready before Remotion captures the frame
|
|
104
|
-
- Errors shown inline in the output
|
|
105
|
-
- Importable: `import {Mermaid} from "@lalalic/markcut/components"`
|
|
106
|
-
|
|
107
|
-
### Wrapping built-ins in custom components
|
|
108
|
-
```js
|
|
109
|
-
import {Markdown, Mermaid} from "@lalalic/markcut/components"
|
|
110
|
-
|
|
111
|
-
export function SuperMarkdown({ source }) {
|
|
112
|
-
return (
|
|
113
|
-
<Markdown
|
|
114
|
-
source={source}
|
|
115
|
-
components={{
|
|
116
|
-
li: ({children}) => <li style={{color:'#ffd700'}}>{children}</li>,
|
|
117
|
-
}}
|
|
118
|
-
/>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
```
|
|
90
|
+
Built-in components available via `@lalalic/markcut/components`. See [docs/components.md](docs/components.md) for full reference.
|
|
122
91
|
|
|
123
|
-
## Common npm packages (used via imports block)
|
|
124
|
-
- `react-markdown` + `remark-gfm` — already bundled as built-in `<Markdown />` above
|
|
125
|
-
- `remark-toc` — generate table of contents
|
|
126
|
-
- `remark-math` + `rehype-katex` — render math formulas with KaTeX
|
|
127
|
-
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
128
|
-
- `@remotion/starburst` — render starburst animations
|
|
129
92
|
|
|
130
93
|
## Golden rule
|
|
131
94
|
- always check stream start and duration to avoid
|
|
@@ -133,8 +96,12 @@ export function SuperMarkdown({ source }) {
|
|
|
133
96
|
- video cut off
|
|
134
97
|
- subtitle mismatch
|
|
135
98
|
- sync issues between audio, video, and subtitles
|
|
136
|
-
|
|
137
|
-
- markcut resolver will automatically calculate the duration based on the audio script length
|
|
138
|
-
- **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
|
|
99
|
+
according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
|
|
139
100
|
- put all manual assets in `assets` folder, such as bgm, logo, watermark, etc. don't put them in `.markcut` folder, which is auto generated and will be deleted when `markcut clean` command is run.
|
|
140
|
-
|
|
101
|
+
|
|
102
|
+
### Don'ts
|
|
103
|
+
- **don't** set duration for script or stream's duration depending on audio script
|
|
104
|
+
- markcut resolver will automatically calculate the duration based on the audio script length
|
|
105
|
+
- **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update
|
|
106
|
+
- **don't** set timeout for `preview`, `vision`, `render` markcut commands, which may take long time to generate medias.
|
|
107
|
+
- **don't** use skill to understand vision media. use `npx @lalalic/markcut vision <folder>`.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
## Built-in Components (no imports needed)
|
|
2
|
+
|
|
3
|
+
These components are available directly in `jsx:".."` fields or importable from the `~~~js imports` block via `@lalalic/markcut/components`:
|
|
4
|
+
|
|
5
|
+
### `<Markdown />` — render markdown content
|
|
6
|
+
```md
|
|
7
|
+
- component jsx:"<Markdown source='# Hello\n\n**bold** text.' />"
|
|
8
|
+
```
|
|
9
|
+
- Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
|
|
10
|
+
- Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
|
|
11
|
+
- ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
|
|
12
|
+
- **`highlight`** — array of 0-based list item indices to highlight (e.g., `highlight={[0,2]}`).
|
|
13
|
+
Matching `<li>` elements receive CSS class `highlight-list-item` with a default golden
|
|
14
|
+
left-border and background tint. Works for both ordered and unordered lists.
|
|
15
|
+
```md
|
|
16
|
+
<Markdown highlight={[0,2]}
|
|
17
|
+
source={"1. First item\n2. Second item\n3. Third item\n4. Fourth item"} />
|
|
18
|
+
```
|
|
19
|
+
- Importable: `import {Markdown} from "@lalalic/markcut/components"`
|
|
20
|
+
|
|
21
|
+
### `<Mermaid />` — render Mermaid diagrams as SVG
|
|
22
|
+
```md
|
|
23
|
+
- component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
|
|
24
|
+
```
|
|
25
|
+
- Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
|
|
26
|
+
- **`highlight`** — node name(s) to highlight. String or array of strings.
|
|
27
|
+
Toggles CSS class `highlight` on matching SVG elements. The diagram source
|
|
28
|
+
must define the class via `classDef`:
|
|
29
|
+
```md
|
|
30
|
+
classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px,color:#000
|
|
31
|
+
```
|
|
32
|
+
- **`animateEdges`** — edge(s) to animate with a flowing dash effect.
|
|
33
|
+
`true` = animate all edges. `string[]` = specific edges by source→target alias:
|
|
34
|
+
```md
|
|
35
|
+
animateEdges={["A->B","D->F","F->G"]}
|
|
36
|
+
```
|
|
37
|
+
Animates matching edge `<path>` elements via CSS `stroke-dasharray` + `stroke-dashoffset` keyframes.
|
|
38
|
+
- Source from code fences: use `~~~mermaid` to define the diagram source in a
|
|
39
|
+
separate block, then reference it as `source={mermaid}` in the JSX:
|
|
40
|
+
```md
|
|
41
|
+
- component id:flowChart duration:12
|
|
42
|
+
~~~jsx
|
|
43
|
+
<Mermaid highlight={highlight} animateEdges={animateEdges}
|
|
44
|
+
theme='dark' source={mermaid}/>
|
|
45
|
+
~~~
|
|
46
|
+
~~~mermaid
|
|
47
|
+
graph TD
|
|
48
|
+
A["Receive Request"] --> B["Validate Input"]
|
|
49
|
+
B --> C{"Valid?"}
|
|
50
|
+
classDef highlight fill:#ffd700,stroke:#ff6600,stroke-width:3px
|
|
51
|
+
~~~
|
|
52
|
+
highlight:"A"
|
|
53
|
+
animateEdges:true
|
|
54
|
+
```
|
|
55
|
+
- Dynamic highlight/edges via events: define `on` specs that mutate the
|
|
56
|
+
component's registered id:
|
|
57
|
+
```md
|
|
58
|
+
- event duration:3 start:3 on:(start, flowChart.highlight="B")
|
|
59
|
+
- event duration:3 start:6 on:(start, flowChart.highlight="C")
|
|
60
|
+
- event duration:3 start:9 on:(start, flowChart.highlight=["D","G"])
|
|
61
|
+
```
|
|
62
|
+
- Errors shown inline in the output
|
|
63
|
+
- Importable: `import {Mermaid} from "@lalalic/markcut/components"`
|
|
64
|
+
|
|
65
|
+
### Wrapping built-ins in custom components
|
|
66
|
+
```js
|
|
67
|
+
import {Markdown, Mermaid} from "@lalalic/markcut/components"
|
|
68
|
+
|
|
69
|
+
export function SuperMarkdown({ source }) {
|
|
70
|
+
return (
|
|
71
|
+
<Markdown
|
|
72
|
+
source={source}
|
|
73
|
+
components={{
|
|
74
|
+
li: ({children}) => <li style={{color:'#ffd700'}}>{children}</li>,
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Common npm packages (used via imports block)
|
|
82
|
+
- `react-markdown` + `remark-gfm` — already bundled as built-in `<Markdown />` above
|
|
83
|
+
- `remark-toc` — generate table of contents
|
|
84
|
+
- `remark-math` + `rehype-katex` — render math formulas with KaTeX
|
|
85
|
+
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
86
|
+
- `@remotion/starburst` — render starburst animations
|
|
87
|
+
- `react-webcam-pro` — render webcam video
|
|
88
|
+
- `react-chartjs-2` — render charts with Chart.js at `https://react-chartjs-2.js.org/components`
|
|
89
|
+
- `@xyflow/react` - render diagrams with `https://reactflow.dev/api-reference`
|
|
@@ -69,6 +69,7 @@ Where:
|
|
|
69
69
|
| `audio` | `audio` | Voiceover, BGM, SFX | `src` + (`duration` or `endAt`) |
|
|
70
70
|
| `script "..."` | `audio` | Narration/TTS (shorthand for audio with script) | `script` text (the `"..."` is the primary content) |
|
|
71
71
|
| `component` | `component` | JSX React component | `jsx` (inline or code fence) + `duration` (unless `isBackground`) |
|
|
72
|
+
| `event` | `component` (stub) | **Event-only** — renders nothing, fires JS on registered components at a specific frame | `on:(when, state)` + `start` + `duration` |
|
|
72
73
|
| `rhythm` | `rhythm` | Beat-synced audio with timed children | `src`, `spots`, `children` |
|
|
73
74
|
| `map` | `map` | Animated route visualization | `duration`, `waypoints` |
|
|
74
75
|
| `include` | `include` | Embed external `.md` sub-video | `src` (path to another `.md` file) |
|
|
@@ -112,10 +113,13 @@ Fire JS expression at a specific frame to mutate component state.
|
|
|
112
113
|
```
|
|
113
114
|
- script "Narration" on:(start, slide.current=0)
|
|
114
115
|
- script "Beat" on:(50%, slide.current++)
|
|
116
|
+
- event duration:3 start:6 on:(start, slide.current=1)
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
`when`: `start`/`end`/`50%` (percent) / `2.5s` (seconds value). `state`: any JS expression.
|
|
118
120
|
|
|
121
|
+
Use the `event` type token to create an **event-only stub** — a node that renders nothing and exists solely to fire an event at a specific time. This avoids boilerplate like `- component duration:3 jsx:"<></>" start:6 on:(...)`.
|
|
122
|
+
|
|
119
123
|
#### Lists — `spots:[n,n,n]`, `start:n`
|
|
120
124
|
|
|
121
125
|
```
|
|
@@ -160,8 +164,7 @@ When a property value is too long for a single line (JSX, prompts, scripts, mark
|
|
|
160
164
|
| `~~~jsx jsx` or `~~~jsx` | `jsx` | Component JSX expression |
|
|
161
165
|
| `~~~prompt prompt` | `prompt` | TTI/TTV generation prompt |
|
|
162
166
|
| `~~~script script` or `~~~script` | `script` | Narration text on audio nodes |
|
|
163
|
-
| `~~~css stylesheet` | `stylesheet` | Global CSS (only valid at root level) |
|
|
164
|
-
| `~~~md <key>` | arbitrary | Markdown content for a specific key (e.g., `source` for `react-markdown`) |
|
|
167
|
+
| `~~~css stylesheet` | `stylesheet` | Global CSS (only valid at root level) || `~~~mermaid` | `mermaid` | Mermaid diagram source (referenced as `{mermaid}` in JSX) || `~~~md <key>` | arbitrary | Markdown content for a specific key (e.g., `source` for `react-markdown`) |
|
|
165
168
|
|
|
166
169
|
### 7. Scene Metadata Block
|
|
167
170
|
|
|
@@ -331,6 +334,9 @@ Subtitles are configured at the root level as a VTT overlay. Set via `subtitle:`
|
|
|
331
334
|
| `zoom` | int (default 10) | map |
|
|
332
335
|
| `center` | `{lat:n,lng:n}` JSON | map |
|
|
333
336
|
| `mapType` | `roadmap\|satellite\|hybrid\|terrain` | map |
|
|
337
|
+
| `language` | map label/UI language. Supports `en`, `zh`, `zh-CN`, etc. | map |
|
|
338
|
+
| `lang` | alias of `language` (e.g. `lang:zh`) | map |
|
|
339
|
+
| `region` | region code hint (e.g. `CN`, `US`) | map |
|
|
334
340
|
| `routeMarker` | emoji string e.g. `"🚗"` | map |
|
|
335
341
|
| `title` | display title | scene |
|
|
336
342
|
| `instruction` | visual intent / style / any prompt; NOT rendered | any |
|
|
@@ -433,6 +439,15 @@ When: animated route. Required: `duration`, `waypoints`.
|
|
|
433
439
|
|
|
434
440
|
`- map duration:3 travelMode:DRIVING waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]`
|
|
435
441
|
|
|
442
|
+
Language and region are optional:
|
|
443
|
+
|
|
444
|
+
`- map duration:3 language:zh region:CN waypoints:[31.23,121.47,"上海";39.90,116.40,"北京"]`
|
|
445
|
+
|
|
446
|
+
Shorthand aliases:
|
|
447
|
+
|
|
448
|
+
- `language:zh` or `lang:zh` → normalized to `zh-CN` with `region:CN` default
|
|
449
|
+
- `language:en` or `lang:en` → keeps English labels with `region:US` default
|
|
450
|
+
|
|
436
451
|
### `include`
|
|
437
452
|
|
|
438
453
|
When: embed an external markdown file as a sub-video. The sub-video is
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 🎧 Remotion Sound Effects
|
|
2
|
+
|
|
3
|
+
## 1. URL Construction
|
|
4
|
+
|
|
5
|
+
All sound effect URLs follow this pattern:
|
|
6
|
+
|
|
7
|
+
`https://remotion.media/<name>.wav`
|
|
8
|
+
|
|
9
|
+
such as `https://remotion.media/sanctuary-guardian-what.wav`
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 2. Available Names
|
|
13
|
+
|
|
14
|
+
whip
|
|
15
|
+
whoosh
|
|
16
|
+
page-turn
|
|
17
|
+
switch
|
|
18
|
+
mouse-click
|
|
19
|
+
shutter-modern
|
|
20
|
+
shutter-old
|
|
21
|
+
ding
|
|
22
|
+
bruh
|
|
23
|
+
vine-boom
|
|
24
|
+
windows-xp-error
|
|
25
|
+
fah
|
|
26
|
+
spongebob-fail
|
|
27
|
+
omg-hell-nah
|
|
28
|
+
price-is-right-fail
|
|
29
|
+
romance-meme
|
|
30
|
+
bone-crack
|
|
31
|
+
anime-wow
|
|
32
|
+
yippee
|
|
33
|
+
loading-lag
|
|
34
|
+
wilhelm-scream
|
|
35
|
+
mac-quack
|
|
36
|
+
skedaddle
|
|
37
|
+
snapchat-notification
|
|
38
|
+
nelly-ahh
|
|
39
|
+
sanctuary-guardian-what
|
|
40
|
+
minecraft-hurt
|
|
41
|
+
oh-my-god-vine
|
|
42
|
+
illuminati-confirmed
|
|
43
|
+
dramatic-boomer
|
|
44
|
+
triggered
|
|
45
|
+
record-scratch
|
|
@@ -11,39 +11,153 @@ import { Mermaid } from "./Mermaid";
|
|
|
11
11
|
*
|
|
12
12
|
* The `source` prop is the markdown string to render.
|
|
13
13
|
*
|
|
14
|
+
* `highlight` is an array of 0-based list item indices to highlight.
|
|
15
|
+
* Works for both ordered (`ol`) and unordered (`ul`) lists. Matching
|
|
16
|
+
* `<li>` elements get CSS class `highlight-list-item` (intended for use
|
|
17
|
+
* with a `classDef` or a custom stylesheet).
|
|
18
|
+
*
|
|
14
19
|
* Built-in — no imports or frontmatter registration needed.
|
|
15
20
|
*/
|
|
16
21
|
export interface MarkdownProps {
|
|
17
|
-
|
|
22
|
+
children?: string;
|
|
18
23
|
source?: string;
|
|
19
24
|
className?: string;
|
|
20
25
|
plugins?: any[];
|
|
21
26
|
components?: any;
|
|
27
|
+
/** 0-based indices of list items to highlight. */
|
|
28
|
+
highlight?: number[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Context to pass highlightList down to list renderers
|
|
32
|
+
const ListCtx = React.createContext<number[]>([]);
|
|
33
|
+
|
|
34
|
+
function OrderedList({
|
|
35
|
+
children,
|
|
36
|
+
...props
|
|
37
|
+
}: React.ComponentPropsWithoutRef<"ol">) {
|
|
38
|
+
const hl = React.useContext(ListCtx);
|
|
39
|
+
const items = React.Children.toArray(children).filter(
|
|
40
|
+
(c): c is React.ReactElement => React.isValidElement(c) && c.type === "li",
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<ol {...props}>
|
|
44
|
+
{items.map((child, i) => {
|
|
45
|
+
if (hl.includes(i)) {
|
|
46
|
+
return React.cloneElement(child, { className: "highlight-list-item", key: (child as any).key } as any);
|
|
47
|
+
}
|
|
48
|
+
return child;
|
|
49
|
+
})}
|
|
50
|
+
</ol>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function UnorderedList({
|
|
55
|
+
children,
|
|
56
|
+
...props
|
|
57
|
+
}: React.ComponentPropsWithoutRef<"ul">) {
|
|
58
|
+
const hl = React.useContext(ListCtx);
|
|
59
|
+
const items = React.Children.toArray(children).filter(
|
|
60
|
+
(c): c is React.ReactElement => React.isValidElement(c) && c.type === "li",
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<ul {...props}>
|
|
64
|
+
{items.map((child, i) => {
|
|
65
|
+
if (hl.includes(i)) {
|
|
66
|
+
return React.cloneElement(child, { className: "highlight-list-item", key: (child as any).key } as any);
|
|
67
|
+
}
|
|
68
|
+
return child;
|
|
69
|
+
})}
|
|
70
|
+
</ul>
|
|
71
|
+
);
|
|
22
72
|
}
|
|
23
73
|
|
|
24
|
-
export function Markdown({
|
|
74
|
+
export function Markdown({
|
|
75
|
+
children,
|
|
76
|
+
source = children,
|
|
77
|
+
className,
|
|
78
|
+
plugins,
|
|
79
|
+
components: propComponents,
|
|
80
|
+
highlight,
|
|
81
|
+
}: MarkdownProps) {
|
|
82
|
+
const hl = React.useMemo(() => highlight ?? [], [highlight]);
|
|
83
|
+
|
|
84
|
+
// Inject default styles once
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
const id = "markcut-markdown-defaults";
|
|
87
|
+
if (document.getElementById(id)) return;
|
|
88
|
+
const el = document.createElement("style");
|
|
89
|
+
el.id = id;
|
|
90
|
+
el.textContent = `
|
|
91
|
+
.highlight-list-item {
|
|
92
|
+
background: rgba(255, 215, 0, 0.15);
|
|
93
|
+
border-left: 3px solid #ffd700;
|
|
94
|
+
padding-left: 8px;
|
|
95
|
+
border-radius: 0 4px 4px 0;
|
|
96
|
+
}
|
|
97
|
+
.slide {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
padding: 24px;
|
|
103
|
+
text-align: center;
|
|
104
|
+
}
|
|
105
|
+
.slide h1 { font-size: 2em; margin: 0.4em 0; font-weight: 700; }
|
|
106
|
+
.slide h2 { font-size: 1.6em; margin: 0.35em 0; font-weight: 600; }
|
|
107
|
+
.slide h3 { font-size: 1.3em; margin: 0.3em 0; font-weight: 600; }
|
|
108
|
+
.slide p { margin: 0.6em 0; line-height: 1.6; }
|
|
109
|
+
.slide ul, .slide ol { margin: 0.5em 0; padding-left: 1.5em; text-align: left; }
|
|
110
|
+
.slide li { margin: 0.3em 0; }
|
|
111
|
+
.slide blockquote {
|
|
112
|
+
margin: 0.6em 0;
|
|
113
|
+
padding: 0.4em 1em;
|
|
114
|
+
border-left: 3px solid rgba(255,255,255,.3);
|
|
115
|
+
font-style: italic;
|
|
116
|
+
opacity: .85;
|
|
117
|
+
}
|
|
118
|
+
.slide code {
|
|
119
|
+
background: rgba(255,255,255,.08);
|
|
120
|
+
padding: 0.15em 0.4em;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
font-size: 0.9em;
|
|
123
|
+
}
|
|
124
|
+
.slide pre { margin: 0.6em 0; text-align: left; width: 100%; }
|
|
125
|
+
.slide a { color: #4a9eff; text-decoration: none; }
|
|
126
|
+
.slide a:hover { text-decoration: underline; }
|
|
127
|
+
`;
|
|
128
|
+
document.head.appendChild(el);
|
|
129
|
+
return () => { document.getElementById(id)?.remove(); };
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const mergedComponents = React.useMemo(
|
|
133
|
+
() => ({
|
|
134
|
+
...propComponents,
|
|
135
|
+
ol: OrderedList,
|
|
136
|
+
ul: UnorderedList,
|
|
137
|
+
pre: ({ children: preChildren }: { children: React.ReactNode }) => {
|
|
138
|
+
const code = React.Children.toArray(preChildren)[0] as React.ReactElement<any>;
|
|
139
|
+
if (code?.props?.className === "language-mermaid") {
|
|
140
|
+
return <Mermaid source={String(code.props.children)} />;
|
|
141
|
+
}
|
|
142
|
+
if (propComponents?.pre) {
|
|
143
|
+
return (propComponents.pre as any)({ children: preChildren });
|
|
144
|
+
}
|
|
145
|
+
return <pre>{preChildren}</pre>;
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
[propComponents],
|
|
149
|
+
);
|
|
150
|
+
|
|
25
151
|
return (
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} else if (components.pre) {
|
|
37
|
-
return components.pre({ children });
|
|
38
|
-
}
|
|
39
|
-
return <pre>{children}</pre>;
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
42
|
-
[components]
|
|
43
|
-
)}
|
|
44
|
-
>
|
|
45
|
-
{source}
|
|
46
|
-
</ReactMarkdown>
|
|
47
|
-
</div>
|
|
152
|
+
<ListCtx.Provider value={hl}>
|
|
153
|
+
<div className={className}>
|
|
154
|
+
<ReactMarkdown
|
|
155
|
+
remarkPlugins={React.useMemo(() => [remarkGfm, ...(plugins || [])], [plugins])}
|
|
156
|
+
components={mergedComponents}
|
|
157
|
+
>
|
|
158
|
+
{source}
|
|
159
|
+
</ReactMarkdown>
|
|
160
|
+
</div>
|
|
161
|
+
</ListCtx.Provider>
|
|
48
162
|
);
|
|
49
163
|
}
|