@wyttime04/react-vditor 0.1.0 → 1.0.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.
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @wyttime04/react-vditor
2
2
 
3
+ [![GitHub](https://img.shields.io/badge/GitHub-wyttime04%2Freact--vditor-24292f?logo=github)](https://github.com/wyttime04/react-vditor)
4
+ [![npm](https://img.shields.io/npm/v/@wyttime04/react-vditor)](https://www.npmjs.com/package/@wyttime04/react-vditor)
5
+ [![license](https://img.shields.io/npm/l/@wyttime04/react-vditor)](https://github.com/wyttime04/react-vditor/blob/main/LICENSE)
6
+ [![vditor](https://img.shields.io/npm/dependency-version/@wyttime04/react-vditor/vditor)](https://github.com/Vanessa219/vditor)
7
+ [![react](https://img.shields.io/npm/dependency-version/@wyttime04/react-vditor/peer/react)](https://github.com/wyttime04/react-vditor#相容性)
8
+
3
9
  以 [vditor](https://github.com/Vanessa219/vditor) 4.0.0 為基礎的 React 元件,**僅支援 GFM 語法,並可於完全離線的環境運作**。
4
10
 
5
11
  - mermaid、數學式、echarts 等 GFM 以外的附加渲染一律停用,相關區塊以一般程式碼區塊呈現
@@ -62,7 +68,7 @@ const ref = useRef(null);
62
68
  // 僅渲染內容,不建立編輯器
63
69
  <VditorPreview value={markdown}/>
64
70
 
65
- // 轉換為 HTML(GFM 以外的區塊同樣轉為一般程式碼區塊)
71
+ // 轉換為 HTML
66
72
  const html = await md2html(markdown);
67
73
  ```
68
74
 
@@ -163,9 +169,19 @@ props 沿用 `Vditor.preview` 的 `IPreviewOptions`,另增 `value`(要渲染
163
169
 
164
170
  ### `md2html(markdown, options?)`
165
171
 
166
- 將 Markdown 轉換為 HTML,回傳 `Promise<string>`。GFM 以外的區塊會與預覽相同,轉換為一般程式碼區塊
172
+ 將 Markdown 轉換為 HTML,回傳 `Promise<string>`。GFM 以外的區塊與網址不合法的連結,處理方式與預覽相同
167
173
  (vditor 原生的 `md2html` 不套用 `transform`,因此由本函式處理)。
168
174
 
175
+ ### `gfmTransform`, `linkTransform`, `previewTransform`
176
+
177
+ 預覽輸出所套用的 HTML 轉換,型別皆為 `(html: string) => string`。
178
+
179
+ | 函式 | 說明 |
180
+ |---|---|
181
+ | `gfmTransform` | GFM 以外的區塊(mermaid、數學式等)轉為一般程式碼區塊 |
182
+ | `linkTransform` | 網址不合法的連結轉為純文字,規則詳見「[預設值與原生 vditor 的差異](#預設值與原生-vditor-的差異)」 |
183
+ | `previewTransform` | 依序套用 `gfmTransform`、`linkTransform`;`<VditorPreview>`、`md2html()` 與編輯器的預覽區皆使用此轉換 |
184
+
169
185
  ## 主題
170
186
 
171
187
  `<Vditor>` 與 `<VditorPreview>` 提供 `light`, `dark` 兩種主題,另可設為 `auto`,依作業系統的外觀設定(`prefers-color-scheme`)自動切換,並在系統設定變更時即時套用。
@@ -214,14 +230,15 @@ const App = () => {
214
230
  |---|---|---|
215
231
  | 編輯器外框 | vditor `classic` | vditor `dark` |
216
232
  | 內容主題 | vditor `light` | vditor `dark` |
217
- | 程式碼上色 | highlight.js `github` | highlight.js `github-dark` |
233
+ | 程式碼上色 | highlight.js `github`(vditor 整頁載入的樣式) | highlight.js `github-dark` |
234
+ | 預覽的程式碼區塊背景色 | `#f6f8fa` | `#0d1117`(`github-dark`) |
218
235
  | `<VditorPreview>` 背景色 | 不設定 | `#2f363d`(與 vditor 深色編輯區相同) |
219
236
 
220
237
  ### 說明
221
238
 
222
239
  - 各元件的主題彼此獨立,同一頁面可混用不同主題。vditor 原生以整頁共用的 `<link>` 載入內容主題,
223
240
  同一頁面只能使用一種;本套件將主題樣式局部化並併入 `index.css`,改由元件容器上的
224
- `react-vditor--light` / `react-vditor--dark` class 套用。
241
+ `react-vditor--light` / `react-vditor--dark` class 套用。局部化不提高選擇器權重,除上表的預覽程式碼區塊背景色外,套用結果與原生 vditor 相同。
225
242
  - 切換主題不會重建編輯器。
226
243
  - `theme` prop 取代 vditor 原生的 `theme`(`classic` / `dark`)與 `preview.theme` 設定;ref 亦不提供 `setTheme`。
227
244
 
@@ -234,7 +251,7 @@ const App = () => {
234
251
  "headings", "bold", "italic", "strike", "link", "|",
235
252
  "list", "ordered-list", "check", "outdent", "indent", "|",
236
253
  "quote", "line", "code", "inline-code", "insert-before", "insert-after", "|",
237
- "upload-image", "table", "|",
254
+ "upload", "table", "|",
238
255
  "undo", "redo", "|",
239
256
  "fullscreen", "edit-mode",
240
257
  {name: "more", toolbar: ["both", "outline", "preview"]},
@@ -244,14 +261,13 @@ const App = () => {
244
261
  與 vditor 原生的預設工具列相比:
245
262
 
246
263
  - 移除 `export`(寫死連線至 CDN)、`devtools`(依賴 echarts)、`info`(寫死 unpkg 的圖片網址)
247
- - 移除 `emoji`, `record`, `upload`, `code-theme`, `content-theme`, `help`
248
- - 新增 `upload-image`
264
+ - 移除 `emoji`, `record`, `code-theme`, `content-theme`, `help`
249
265
 
250
266
  傳入 `toolbar` prop 時,會完整取代預設工具列。
251
267
 
252
- ### 上傳圖片按鈕 `upload-image`
268
+ ### 上傳
253
269
 
254
- 僅接受圖片的上傳按鈕,已包含在預設工具列中,並可與 vditor 內建的 `upload`(圖片或文件)同時使用。
270
+ 工具列的 `upload` 按鈕、拖放與貼上加入的檔案,皆交由 `upload.handler` 處理,預設僅接受圖片。
255
271
 
256
272
  #### 範例
257
273
 
@@ -272,35 +288,35 @@ const editorRef = useRef(null);
272
288
  />
273
289
  ```
274
290
 
275
- 使用自訂的 `toolbar` 時,請將 `"upload-image"` 加入其中:
291
+ 允許上傳文件時,將 `upload.imageOnly` 設為 `false`:
276
292
 
277
293
  ```jsx
278
- <Vditor toolbar={["bold", "italic", "|", "upload-image"]} upload={{handler}}/>
294
+ <Vditor upload={{handler, imageOnly: false, accept: "image/*,.pdf"}}/>
279
295
  ```
280
296
 
281
297
  #### 行為
282
298
 
283
- 1. 點選按鈕後開啟檔案選擇視窗,僅列出圖片檔
284
- 2. 選取的檔案再次過濾,僅保留圖片,交由 `upload.handler` 處理
299
+ 1. 檔案依 `upload.imageOnly` 與 `upload.accept` 過濾,僅將符合者交由 `upload.handler` 處理
300
+ 2. 全部不符合時不呼叫 `handler`,並顯示「檔案類型不允許上傳」
285
301
  3. `handler` 回傳字串時,以提示訊息顯示(例如「檔案過大」)
286
302
 
287
- 此行為與 vditor 內建上傳在設定 `handler` 時相同。插入圖片語法由 `handler` 負責,例如呼叫 ref 的 `insertValue`。
303
+ 插入圖片或連結語法由 `handler` 負責,例如呼叫 ref 的 `insertValue`。
288
304
 
289
305
  #### 相關設定
290
306
 
291
307
  | 設定 | 說明 |
292
308
  |---|---|
293
- | `upload.handler` | 必填。未設定時,點選按鈕僅顯示設定提示,不開啟檔案選擇視窗 |
294
- | `upload.multiple` | 為 `true`(vditor 預設)時可一次選取多張圖片,否則僅能選取一張 |
295
- | `lang` | 按鈕的提示文字依語言顯示,支援 zh_TW、zh_CN、en_US,其他語言顯示英文 |
296
- | 以物件指定按鈕 | 如需自訂提示文字或圖示:`{name: "upload-image", tip: "…", icon: "<svg>…</svg>"}`,指定的欄位優先於預設值。自訂的 `tip` 為固定字串,不會隨語言切換 |
309
+ | `upload.imageOnly` | 本套件新增,預設 `true`:僅接受圖片,按鈕提示為「上傳圖片」(支援 zh_TW、zh_CN、en_US,其他語言顯示英文)。設為 `false` 時僅依 `upload.accept` 過濾,按鈕提示為 vditor 的「上傳圖片或文件」 |
310
+ | `upload.accept` | 格式同 `<input accept>`,例如 `image/png,.pdf`。`imageOnly` 為 `true` 時僅能進一步限縮圖片類型;為 `false` 且未設定時不限制 |
311
+ | `upload.handler` | 處理上傳的檔案。上述過濾僅作用於 `handler`;改用 vditor 的 `upload.url` 上傳時,由 vditor 依 `upload.accept` 檢查 |
312
+ | `upload.multiple` | 為 `true`(vditor 預設)時可一次選取多個檔案,否則僅能選取一個 |
313
+ | 以物件指定按鈕 | 如需自訂提示文字或圖示:`{name: "upload", tip: "…", icon: "<svg>…</svg>"}`,指定的欄位優先於預設值。自訂的 `tip` 為固定字串,不會隨語言切換 |
297
314
 
298
315
  #### 限制
299
316
 
300
- - 僅支援 `upload.handler`,不支援 `upload.url`:`url` 屬於 vditor 內部的上傳流程,未提供可呼叫的公開 API
301
- - 檔案類型的限制僅作用於檔案選擇視窗;以拖放或貼上方式加入的檔案不經過此按鈕。
302
- 另外,設定 `upload.handler` 時 vditor 不會檢查檔案類型與大小,請於 `handler` 中自行驗證
303
- - `upload.handler` 只在建立編輯器時讀取,詳見「[props 變更時的行為](#props-變更時的行為)」
317
+ - 設定 `upload.handler` 時 vditor 不會檢查檔案大小(`upload.max`),請於 `handler` 中自行驗證
318
+ - `upload.multiple` 不為 `true` 時,vditor 會先只取第一個檔案再交由過濾,同時拖入多個檔案可能因此全部被擋下
319
+ - `upload` 的設定只在建立編輯器時讀取,詳見「[props 變更時的行為](#props-變更時的行為)」
304
320
 
305
321
  ## 預設值與原生 vditor 的差異
306
322
 
@@ -311,19 +327,51 @@ const editorRef = useRef(null);
311
327
  | `theme` | `light` | 僅支援 `light` / `dark` / `auto`,詳見「[主題](#主題)」 |
312
328
  | `toolbar` | 詳見「[預設工具列](#預設工具列)」 | |
313
329
  | `cache` | `{enable: false}` | vditor 啟用快取時必須提供 `cache.id` |
330
+ | `upload.imageOnly` | `true` | 本套件新增的選項:僅接受圖片,詳見「[上傳](#上傳)」 |
314
331
  | `preview.markdown.footnotes` | `false` | GFM 不包含註腳 |
315
332
  | `preview.markdown.codeBlockPreview`, `mathBlockPreview` | `false` | ir / wysiwyg 模式不顯示程式碼區塊的預覽面板,避免渲染 mermaid 等區塊 |
316
333
  | `preview.render.media.enable` | `false` | 不將影片、音訊連結嵌入為 iframe |
317
- | `preview.transform` | GFM 以外的區塊轉為一般程式碼區塊 | 使用端傳入的 `transform` 會於其後執行 |
334
+ | `preview.transform` | GFM 以外的區塊轉為一般程式碼區塊;網址不合法的連結轉為純文字 | 網址規則同 DOMPurify 的 `IS_ALLOWED_URI`:允許 `http(s)`、`mailto`、`tel` 等協定與相對路徑。使用端傳入的 `transform` 會於其後執行 |
318
335
  | `preview.theme.current` | `""`(固定) | 內容主題改由 `theme` 決定,不使用 vditor 整頁共用的 `<link>` |
319
336
  | 行內數學式 `$…$` | 視為一般文字 | GFM 不包含數學式 |
320
337
 
338
+ ## XSS 防護
339
+
340
+ 內建的過濾如下:
341
+
342
+ | 來源 | 處理內容 |
343
+ |---|---|
344
+ | Lute 的 `sanitize`(vditor 預設啟用) | 移除 `<script>`、`<style>`、`on*` 事件屬性與 `javascript:` 網址 |
345
+ | `linkTransform` | 網址不合法的連結轉為純文字,詳見「[預設值與原生 vditor 的差異](#預設值與原生-vditor-的差異)」 |
346
+
347
+ `<iframe>`、`<embed>`、`<form>` 等元素與 `style` 屬性仍會保留,建議透過 `transform` 以 DOMPurify 等工具再行過濾:
348
+
349
+ ```jsx
350
+ import DOMPurify from "dompurify";
351
+
352
+ // iframe、embed、object 由 DOMPurify 預設移除;表單元素與 style 屬性需另行禁止
353
+ const sanitize = (html) => DOMPurify.sanitize(html, {
354
+ FORBID_TAGS: ["form", "button", "textarea", "select"],
355
+ FORBID_ATTR: ["style"],
356
+ });
357
+
358
+ <Vditor preview={{transform: sanitize}}/>
359
+ <VditorPreview value={markdown} transform={sanitize}/>
360
+ const html = await md2html(markdown, {transform: sanitize});
361
+ ```
362
+
363
+ - 使用端的 `transform` 於 `previewTransform` 之後執行
364
+ - 任務清單的勾選框為 `<input type="checkbox">`:需要任務清單時不可禁止 `input`;不需要時可將 `input` 加入 `FORBID_TAGS`
365
+ - 程式碼區塊的複製按鈕與上色由 vditor 於 `transform` 之後加入,不受影響
366
+ - `<Vditor>` 的 `preview.transform` 僅作用於預覽區(sv 模式的分屏預覽與預覽模式);wysiwyg、ir 模式的編輯區不經過 `transform`
367
+
321
368
  ## 已知限制
322
369
 
323
370
  - ir / wysiwyg 模式中的程式碼區塊不會上色(停用預覽面板所致);sv 模式的預覽區與 `<VditorPreview>` 仍會上色
371
+ - 網址不合法的連結轉為純文字,僅作用於 `<VditorPreview>`、`md2html()` 與編輯器的預覽區;ir / wysiwyg 模式的編輯區不套用 `transform`,連結仍會顯示
324
372
  - `$$ … $$` 區塊會以程式碼區塊呈現(Lute 未提供停用此語法的設定)
325
373
  - `:smile:` 等 emoji 代碼會轉為對應字元,YAML front matter 會以程式碼區塊呈現;兩者雖非 GFM 語法,仍予以保留
326
- - vditor 無論設定為何,都會整頁載入 highlight.js 的 `github.min.css`;`dark` 主題的程式碼樣式以局部化且優先權較高的規則覆寫
374
+ - vditor 無論設定為何,都會整頁載入 highlight.js 的 `github.min.css`;`light` 主題直接沿用,`dark` 主題的程式碼樣式以權重略高的局部化規則覆寫
327
375
  - 啟用 `cache` 且未提供 `value` 時,若快取內容包含 `$…$`,初次渲染會嘗試載入未輸出的 katex,產生本機 404
328
376
 
329
377
  ## 開發
@@ -340,9 +388,9 @@ npm test # 建置測試頁、檢查型別,並執行 puppeteer 測
340
388
 
341
389
  ### 開發用畫面
342
390
 
343
- `dev/` 提供開發用畫面:左側為 `<Vditor>`,右側為 `<VditorPreview>` 與 `md2html()` 的輸出,下方列出事件紀錄。
344
- 上方可切換主題(透過 `VditorThemeProvider`),並呼叫 ref 的 `insertValue`, `disabled` / `enable`, `getValue`。
345
- 工具列的「上傳圖片」不會實際上傳,選取的圖片以 `blob:` 網址直接顯示。
391
+ `dev/` 提供開發用畫面,展示 `<Vditor>`、`<VditorPreview>` 與 `md2html()` 的輸出,並列出編輯器的事件紀錄。
392
+ 可切換主題(透過 `VditorThemeProvider`),並呼叫 ref 的 `insertValue`, `disabled` / `enable`, `getValue`。
393
+ 上傳的圖片(工具列、拖放、貼上)不會實際上傳,以 `blob:` 網址直接顯示。
346
394
 
347
395
  `dev/main.jsx` 與使用端相同,以 `import "@wyttime04/react-vditor"` 及 `import "@wyttime04/react-vditor/index.css"` 引用套件,
348
396
  由 `dev/webpack.config.cjs` 依 mode 決定來源:
package/dist/Vditor.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as React from "react";
2
2
  import VditorCore from "vditor";
3
- import { defaultCdn } from "./cdn.js";
4
- import { gfmEditorOptions } from "./gfm.js";
3
+ import { editorOptions } from "./options/default.js";
5
4
  import { themeClassName, useResolvedTheme } from "./theme.js";
6
5
 
7
6
  /**
@@ -93,11 +92,11 @@ export const Vditor = /*#__PURE__*/React.forwardRef(function Vditor(props, ref)
93
92
  } = propsRef.current;
94
93
  // 先用空內容建立,after 裡關掉行內數學式之後才放入真正的內容;否則初次渲染就會去載入 katex
95
94
  const instance = new VditorCore(host, {
96
- ...gfmEditorOptions({
95
+ ...editorOptions({
97
96
  ...options,
98
97
  value: "",
99
98
  theme: editorTheme(themeRef.current)
100
- }, options.cdn || defaultCdn()),
99
+ }),
101
100
  ...callbacks
102
101
  });
103
102
  return () => {
@@ -162,7 +161,7 @@ export const Vditor = /*#__PURE__*/React.forwardRef(function Vditor(props, ref)
162
161
  return contentRef.current ?? "";
163
162
  }
164
163
  if (name === "getCurrentMode") {
165
- return propsRef.current.mode || "wysiwyg"; // 跟 gfm.js 的預設 mode 一致
164
+ return propsRef.current.mode || "wysiwyg"; // 跟 options/default.js 的預設 mode 一致
166
165
  }
167
166
  return undefined;
168
167
  };
@@ -1,7 +1,6 @@
1
1
  import * as React from "react";
2
2
  import VditorCore from "vditor";
3
- import { defaultCdn } from "./cdn.js";
4
- import { gfmPreviewOptions } from "./gfm.js";
3
+ import { previewOptions } from "./options/default.js";
5
4
  import { themeClassName, useResolvedTheme } from "./theme.js";
6
5
 
7
6
  /**
@@ -21,7 +20,7 @@ export const VditorPreview = props => {
21
20
  // 選項常常是每次 render 都新建的物件;用內容比較,避免每次 render 都重新渲染
22
21
  const optionsKey = JSON.stringify(options);
23
22
  React.useEffect(() => {
24
- VditorCore.preview(elementRef.current, value, gfmPreviewOptions(options, options.cdn || defaultCdn()));
23
+ VditorCore.preview(elementRef.current, value, previewOptions(options));
25
24
  }, [value, optionsKey]); // options 以內容比較(optionsKey),不放物件本身
26
25
 
27
26
  // 主題只看外層容器的 class(index.css 裡局部化的樣式),換主題不用重新渲染;
package/dist/index.css CHANGED
@@ -1904,7 +1904,7 @@
1904
1904
  }
1905
1905
 
1906
1906
 
1907
- /* react-vditor light / dark 主題,只作用在 .react-vditor--light / .react-vditor--dark 容器裡 */
1907
+ /* react-vditor:light / dark 主題 */
1908
1908
  /*!
1909
1909
  * Vditor - A markdown editor written in TypeScript.
1910
1910
  *
@@ -1931,7 +1931,7 @@
1931
1931
  * SOFTWARE.
1932
1932
  *
1933
1933
  */
1934
- .react-vditor--light .vditor-reset {
1934
+ :where(.react-vditor--light) .vditor-reset {
1935
1935
  --callout-note-color: #0969da;
1936
1936
  --callout-note-background-color: rgba(9, 105, 218, 0.08);
1937
1937
  --callout-tip-color: #1a7f37;
@@ -1944,93 +1944,82 @@
1944
1944
  --callout-caution-background-color: rgba(207, 34, 46, 0.08);
1945
1945
  }
1946
1946
 
1947
- .react-vditor--light .vditor-reset h1, .react-vditor--light .vditor-reset h2 {
1947
+ :where(.react-vditor--light) .vditor-reset h1, :where(.react-vditor--light) .vditor-reset h2 {
1948
1948
  padding-bottom: 0.3em;
1949
1949
  border-bottom: 1px solid #eaecef;
1950
1950
  }
1951
1951
 
1952
- .react-vditor--light .vditor-reset hr {
1952
+ :where(.react-vditor--light) .vditor-reset hr {
1953
1953
  background-color: #eaecef;
1954
1954
  }
1955
1955
 
1956
- .react-vditor--light .vditor-reset blockquote {
1956
+ :where(.react-vditor--light) .vditor-reset blockquote {
1957
1957
  color: #6a737d;
1958
1958
  border-left: .25em solid #eaecef
1959
1959
  }
1960
1960
 
1961
- .react-vditor--light .vditor-reset iframe {
1961
+ :where(.react-vditor--light) .vditor-reset iframe {
1962
1962
  border: 1px solid #d1d5da
1963
1963
  }
1964
1964
 
1965
- .react-vditor--light .vditor-reset table tr {
1965
+ :where(.react-vditor--light) .vditor-reset table tr {
1966
1966
  border-top: 1px solid #c6cbd1;
1967
1967
  background-color: #fafbfc
1968
1968
  }
1969
1969
 
1970
- .react-vditor--light .vditor-reset table td, .react-vditor--light .vditor-reset table th {
1970
+ :where(.react-vditor--light) .vditor-reset table td, :where(.react-vditor--light) .vditor-reset table th {
1971
1971
  border: 1px solid #dfe2e5
1972
1972
  }
1973
1973
 
1974
- .react-vditor--light .vditor-reset table tbody tr:nth-child(2n) {
1974
+ :where(.react-vditor--light) .vditor-reset table tbody tr:nth-child(2n) {
1975
1975
  background-color: #fff
1976
1976
  }
1977
1977
 
1978
- .react-vditor--light .vditor-reset code:not(.hljs):not(.highlight-chroma) {
1978
+ :where(.react-vditor--light) .vditor-reset code:not(.hljs):not(.highlight-chroma) {
1979
1979
  background-color: rgba(27, 31, 35, .05);
1980
1980
  }
1981
1981
 
1982
- .react-vditor--light .vditor-reset kbd {
1982
+ :where(.react-vditor--light) .vditor-reset kbd {
1983
1983
  color: #24292e;
1984
1984
  background-color: #fafbfc;
1985
1985
  border: solid 1px #d1d5da;
1986
1986
  box-shadow: inset 0 -1px 0 #d1d5da;
1987
1987
  }
1988
1988
 
1989
- .react-vditor--light .vditor-speech {
1989
+ :where(.react-vditor--light) .vditor-speech {
1990
1990
  background-color: #f6f8fa;
1991
1991
  border: 1px solid #d1d5da;
1992
1992
  color: #586069;
1993
1993
  }
1994
1994
 
1995
- .react-vditor--light .vditor-speech--current, .react-vditor--light .vditor-speech:hover {
1995
+ :where(.react-vditor--light) .vditor-speech--current, :where(.react-vditor--light) .vditor-speech:hover {
1996
1996
  color: #4285f4;
1997
1997
  }
1998
1998
 
1999
- .react-vditor--light .vditor-linkcard a {
1999
+ :where(.react-vditor--light) .vditor-linkcard a {
2000
2000
  background-color: #f6f8fa;
2001
2001
  }
2002
2002
 
2003
- .react-vditor--light .vditor-linkcard a:visited .vditor-linkcard__abstract {
2003
+ :where(.react-vditor--light) .vditor-linkcard a:visited .vditor-linkcard__abstract {
2004
2004
  color: rgba(88, 96, 105, 0.36);
2005
2005
  }
2006
2006
 
2007
- .react-vditor--light .vditor-linkcard__title {
2007
+ :where(.react-vditor--light) .vditor-linkcard__title {
2008
2008
  color: #24292e;
2009
2009
  }
2010
2010
 
2011
- .react-vditor--light .vditor-linkcard__abstract {
2011
+ :where(.react-vditor--light) .vditor-linkcard__abstract {
2012
2012
  color: #586069;
2013
2013
  }
2014
2014
 
2015
- .react-vditor--light .vditor-linkcard__site {
2015
+ :where(.react-vditor--light) .vditor-linkcard__site {
2016
2016
  color: #4285f4;
2017
2017
  }
2018
2018
 
2019
- .react-vditor--light .vditor-linkcard__image {
2019
+ :where(.react-vditor--light) .vditor-linkcard__image {
2020
2020
  background-color: rgba(88, 96, 105, 0.36);
2021
2021
  }
2022
2022
 
2023
- .react-vditor--light pre code.hljs{display:block;overflow-x:auto;padding:1em}.react-vditor--light code.hljs{padding:3px 5px}/*!
2024
- Theme: GitHub
2025
- Description: Light theme as seen on github.com
2026
- Author: github.com
2027
- Maintainer: @Hirse
2028
- Updated: 2021-05-15
2029
-
2030
- Outdated base version: https://github.com/primer/github-syntax-light
2031
- Current colors taken from GitHub's CSS
2032
- */.react-vditor--light .hljs{color:#24292e;background:#fff}.react-vditor--light .hljs-doctag,.react-vditor--light .hljs-keyword,.react-vditor--light .hljs-meta .hljs-keyword,.react-vditor--light .hljs-template-tag,.react-vditor--light .hljs-template-variable,.react-vditor--light .hljs-type,.react-vditor--light .hljs-variable.language_{color:#d73a49}.react-vditor--light .hljs-title,.react-vditor--light .hljs-title.class_,.react-vditor--light .hljs-title.class_.inherited__,.react-vditor--light .hljs-title.function_{color:#6f42c1}.react-vditor--light .hljs-attr,.react-vditor--light .hljs-attribute,.react-vditor--light .hljs-literal,.react-vditor--light .hljs-meta,.react-vditor--light .hljs-number,.react-vditor--light .hljs-operator,.react-vditor--light .hljs-selector-attr,.react-vditor--light .hljs-selector-class,.react-vditor--light .hljs-selector-id,.react-vditor--light .hljs-variable{color:#005cc5}.react-vditor--light .hljs-meta .hljs-string,.react-vditor--light .hljs-regexp,.react-vditor--light .hljs-string{color:#032f62}.react-vditor--light .hljs-built_in,.react-vditor--light .hljs-symbol{color:#e36209}.react-vditor--light .hljs-code,.react-vditor--light .hljs-comment,.react-vditor--light .hljs-formula{color:#6a737d}.react-vditor--light .hljs-name,.react-vditor--light .hljs-quote,.react-vditor--light .hljs-selector-pseudo,.react-vditor--light .hljs-selector-tag{color:#22863a}.react-vditor--light .hljs-subst{color:#24292e}.react-vditor--light .hljs-section{color:#005cc5;font-weight:700}.react-vditor--light .hljs-bullet{color:#735c0f}.react-vditor--light .hljs-emphasis{color:#24292e;font-style:italic}.react-vditor--light .hljs-strong{color:#24292e;font-weight:700}.react-vditor--light .hljs-addition{color:#22863a;background-color:#f0fff4}.react-vditor--light .hljs-deletion{color:#b31d28;background-color:#ffeef0}
2033
-
2034
2023
  /*!
2035
2024
  * Vditor - A markdown editor written in TypeScript.
2036
2025
  *
@@ -2057,7 +2046,7 @@
2057
2046
  * SOFTWARE.
2058
2047
  *
2059
2048
  */
2060
- .react-vditor--dark .vditor-reset {
2049
+ :where(.react-vditor--dark) .vditor-reset {
2061
2050
  color: #d1d5da;
2062
2051
  --callout-note-color: #58a6ff;
2063
2052
  --callout-note-background-color: rgba(56, 139, 253, 0.15);
@@ -2071,101 +2060,101 @@
2071
2060
  --callout-caution-background-color: rgba(248, 81, 73, 0.15);
2072
2061
  }
2073
2062
 
2074
- .react-vditor--dark .vditor-reset a, .react-vditor--dark .vditor-ir__link {
2063
+ :where(.react-vditor--dark) .vditor-reset a, :where(.react-vditor--dark) .vditor-ir__link {
2075
2064
  color: #4285f4;
2076
2065
  }
2077
2066
 
2078
- .react-vditor--dark .vditor-reset h1, .react-vditor--dark .vditor-reset h2 {
2067
+ :where(.react-vditor--dark) .vditor-reset h1, :where(.react-vditor--dark) .vditor-reset h2 {
2079
2068
  padding-bottom: 0.3em;
2080
2069
  border-bottom: 1px solid #d1d5da
2081
2070
  }
2082
2071
 
2083
- .react-vditor--dark .vditor-reset hr {
2072
+ :where(.react-vditor--dark) .vditor-reset hr {
2084
2073
  background-color: #d1d5da
2085
2074
  }
2086
2075
 
2087
- .react-vditor--dark .vditor-reset blockquote {
2076
+ :where(.react-vditor--dark) .vditor-reset blockquote {
2088
2077
  padding: 0 1em;
2089
2078
  color: #b9b9b9;
2090
2079
  border-left: .25em solid #d1d5da
2091
2080
  }
2092
2081
 
2093
- .react-vditor--dark .vditor-reset iframe {
2082
+ :where(.react-vditor--dark) .vditor-reset iframe {
2094
2083
  border: 1px solid #141414
2095
2084
  }
2096
2085
 
2097
- .react-vditor--dark .vditor-reset table tr {
2086
+ :where(.react-vditor--dark) .vditor-reset table tr {
2098
2087
  background-color: #2f363d
2099
2088
  }
2100
2089
 
2101
- .react-vditor--dark .vditor-reset table td, .react-vditor--dark .vditor-reset table th {
2090
+ :where(.react-vditor--dark) .vditor-reset table td, :where(.react-vditor--dark) .vditor-reset table th {
2102
2091
  border: 1px solid #dfe2e5
2103
2092
  }
2104
2093
 
2105
- .react-vditor--dark .vditor-reset table tbody tr:nth-child(2n) {
2094
+ :where(.react-vditor--dark) .vditor-reset table tbody tr:nth-child(2n) {
2106
2095
  background-color: #24292e
2107
2096
  }
2108
2097
 
2109
- .react-vditor--dark .vditor-reset code:not(.hljs):not(.highlight-chroma) {
2098
+ :where(.react-vditor--dark) .vditor-reset code:not(.hljs):not(.highlight-chroma) {
2110
2099
  background-color: rgba(66, 133, 244, .36);
2111
2100
  }
2112
2101
 
2113
- .react-vditor--dark .vditor-reset .language-abc svg,
2114
- .react-vditor--dark .vditor-reset .language-abc path {
2102
+ :where(.react-vditor--dark) .vditor-reset .language-abc svg,
2103
+ :where(.react-vditor--dark) .vditor-reset .language-abc path {
2115
2104
  fill: currentColor;
2116
2105
  color: #d1d5da;
2117
2106
  }
2118
2107
 
2119
- .react-vditor--dark .language-graphviz polygon {
2108
+ :where(.react-vditor--dark) .language-graphviz polygon {
2120
2109
  fill: rgba(66, 133, 244, .36);
2121
2110
  }
2122
2111
 
2123
- .react-vditor--dark .vditor-reset kbd {
2112
+ :where(.react-vditor--dark) .vditor-reset kbd {
2124
2113
  color: #d1d5da;
2125
2114
  background-color: #2f363d;
2126
2115
  border: 1px solid #141414;
2127
2116
  box-shadow: inset 0 -1px 0 #141414
2128
2117
  }
2129
2118
 
2130
- .react-vditor--dark .vditor-copy svg {
2119
+ :where(.react-vditor--dark) .vditor-copy svg {
2131
2120
  color: #b9b9b9
2132
2121
  }
2133
2122
 
2134
- .react-vditor--dark .vditor-speech {
2123
+ :where(.react-vditor--dark) .vditor-speech {
2135
2124
  background-color: #1d2125;
2136
2125
  border: 1px solid #141414;
2137
2126
  color: #b9b9b9
2138
2127
  }
2139
2128
 
2140
- .react-vditor--dark .vditor-speech--current, .react-vditor--dark .vditor-speech:hover {
2129
+ :where(.react-vditor--dark) .vditor-speech--current, :where(.react-vditor--dark) .vditor-speech:hover {
2141
2130
  color: #fff
2142
2131
  }
2143
2132
 
2144
- .react-vditor--dark .vditor-linkcard a {
2133
+ :where(.react-vditor--dark) .vditor-linkcard a {
2145
2134
  background-color: #1d2125;
2146
2135
  }
2147
2136
 
2148
- .react-vditor--dark .vditor-linkcard a:visited .vditor-linkcard__abstract {
2137
+ :where(.react-vditor--dark) .vditor-linkcard a:visited .vditor-linkcard__abstract {
2149
2138
  color: hsla(0, 0%, 72.5%, .36)
2150
2139
  }
2151
2140
 
2152
- .react-vditor--dark .vditor-linkcard__title {
2141
+ :where(.react-vditor--dark) .vditor-linkcard__title {
2153
2142
  color: #d1d5da
2154
2143
  }
2155
2144
 
2156
- .react-vditor--dark .vditor-linkcard__abstract {
2145
+ :where(.react-vditor--dark) .vditor-linkcard__abstract {
2157
2146
  color: #b9b9b9
2158
2147
  }
2159
2148
 
2160
- .react-vditor--dark .vditor-linkcard__site {
2149
+ :where(.react-vditor--dark) .vditor-linkcard__site {
2161
2150
  color: #fff
2162
2151
  }
2163
2152
 
2164
- .react-vditor--dark .vditor-linkcard__image {
2153
+ :where(.react-vditor--dark) .vditor-linkcard__image {
2165
2154
  background-color: hsla(0, 0%, 72.5%, .36)
2166
2155
  }
2167
2156
 
2168
- .react-vditor--dark pre code.hljs{display:block;overflow-x:auto;padding:1em}.react-vditor--dark code.hljs{padding:3px 5px}/*!
2157
+ div:where(.react-vditor--dark) pre code.hljs{display:block;overflow-x:auto;padding:1em}div:where(.react-vditor--dark) code.hljs{padding:3px 5px}/*!
2169
2158
  Theme: GitHub Dark
2170
2159
  Description: Dark theme as seen on github.com
2171
2160
  Author: github.com
@@ -2174,6 +2163,7 @@
2174
2163
 
2175
2164
  Outdated base version: https://github.com/primer/github-syntax-dark
2176
2165
  Current colors taken from GitHub's CSS
2177
- */.react-vditor--dark .hljs{color:#c9d1d9;background:#0d1117}.react-vditor--dark .hljs-doctag,.react-vditor--dark .hljs-keyword,.react-vditor--dark .hljs-meta .hljs-keyword,.react-vditor--dark .hljs-template-tag,.react-vditor--dark .hljs-template-variable,.react-vditor--dark .hljs-type,.react-vditor--dark .hljs-variable.language_{color:#ff7b72}.react-vditor--dark .hljs-title,.react-vditor--dark .hljs-title.class_,.react-vditor--dark .hljs-title.class_.inherited__,.react-vditor--dark .hljs-title.function_{color:#d2a8ff}.react-vditor--dark .hljs-attr,.react-vditor--dark .hljs-attribute,.react-vditor--dark .hljs-literal,.react-vditor--dark .hljs-meta,.react-vditor--dark .hljs-number,.react-vditor--dark .hljs-operator,.react-vditor--dark .hljs-selector-attr,.react-vditor--dark .hljs-selector-class,.react-vditor--dark .hljs-selector-id,.react-vditor--dark .hljs-variable{color:#79c0ff}.react-vditor--dark .hljs-meta .hljs-string,.react-vditor--dark .hljs-regexp,.react-vditor--dark .hljs-string{color:#a5d6ff}.react-vditor--dark .hljs-built_in,.react-vditor--dark .hljs-symbol{color:#ffa657}.react-vditor--dark .hljs-code,.react-vditor--dark .hljs-comment,.react-vditor--dark .hljs-formula{color:#8b949e}.react-vditor--dark .hljs-name,.react-vditor--dark .hljs-quote,.react-vditor--dark .hljs-selector-pseudo,.react-vditor--dark .hljs-selector-tag{color:#7ee787}.react-vditor--dark .hljs-subst{color:#c9d1d9}.react-vditor--dark .hljs-section{color:#1f6feb;font-weight:700}.react-vditor--dark .hljs-bullet{color:#f2cc60}.react-vditor--dark .hljs-emphasis{color:#c9d1d9;font-style:italic}.react-vditor--dark .hljs-strong{color:#c9d1d9;font-weight:700}.react-vditor--dark .hljs-addition{color:#aff5b4;background-color:#033a16}.react-vditor--dark .hljs-deletion{color:#ffdcd7;background-color:#67060c}
2166
+ */div:where(.react-vditor--dark) .hljs{color:#c9d1d9;background:#0d1117}div:where(.react-vditor--dark) .hljs-doctag,div:where(.react-vditor--dark) .hljs-keyword,div:where(.react-vditor--dark) .hljs-meta .hljs-keyword,div:where(.react-vditor--dark) .hljs-template-tag,div:where(.react-vditor--dark) .hljs-template-variable,div:where(.react-vditor--dark) .hljs-type,div:where(.react-vditor--dark) .hljs-variable.language_{color:#ff7b72}div:where(.react-vditor--dark) .hljs-title,div:where(.react-vditor--dark) .hljs-title.class_,div:where(.react-vditor--dark) .hljs-title.class_.inherited__,div:where(.react-vditor--dark) .hljs-title.function_{color:#d2a8ff}div:where(.react-vditor--dark) .hljs-attr,div:where(.react-vditor--dark) .hljs-attribute,div:where(.react-vditor--dark) .hljs-literal,div:where(.react-vditor--dark) .hljs-meta,div:where(.react-vditor--dark) .hljs-number,div:where(.react-vditor--dark) .hljs-operator,div:where(.react-vditor--dark) .hljs-selector-attr,div:where(.react-vditor--dark) .hljs-selector-class,div:where(.react-vditor--dark) .hljs-selector-id,div:where(.react-vditor--dark) .hljs-variable{color:#79c0ff}div:where(.react-vditor--dark) .hljs-meta .hljs-string,div:where(.react-vditor--dark) .hljs-regexp,div:where(.react-vditor--dark) .hljs-string{color:#a5d6ff}div:where(.react-vditor--dark) .hljs-built_in,div:where(.react-vditor--dark) .hljs-symbol{color:#ffa657}div:where(.react-vditor--dark) .hljs-code,div:where(.react-vditor--dark) .hljs-comment,div:where(.react-vditor--dark) .hljs-formula{color:#8b949e}div:where(.react-vditor--dark) .hljs-name,div:where(.react-vditor--dark) .hljs-quote,div:where(.react-vditor--dark) .hljs-selector-pseudo,div:where(.react-vditor--dark) .hljs-selector-tag{color:#7ee787}div:where(.react-vditor--dark) .hljs-subst{color:#c9d1d9}div:where(.react-vditor--dark) .hljs-section{color:#1f6feb;font-weight:700}div:where(.react-vditor--dark) .hljs-bullet{color:#f2cc60}div:where(.react-vditor--dark) .hljs-emphasis{color:#c9d1d9;font-style:italic}div:where(.react-vditor--dark) .hljs-strong{color:#c9d1d9;font-weight:700}div:where(.react-vditor--dark) .hljs-addition{color:#aff5b4;background-color:#033a16}div:where(.react-vditor--dark) .hljs-deletion{color:#ffdcd7;background-color:#67060c}
2178
2167
 
2168
+ :where(.react-vditor--light) .vditor-reset pre > code.hljs { background-color: #f6f8fa; }
2179
2169
  .react-vditor-preview.react-vditor--dark { background-color: #2f363d; }
package/dist/index.d.ts CHANGED
@@ -17,10 +17,17 @@ export type VditorRef = Pick<VditorCore,
17
17
  | "getValue" | "getCurrentMode" | "getSelection" | "getCursorPosition" | "isUploading"
18
18
  | "html2md" | "exportJSON" | "getHTML" | "getCommentIds">;
19
19
 
20
- /** props 就是 vditor 的 IOptions(theme 換成 VditorTheme),另外加 className、style */
21
- export type VditorProps = Omit<IOptions, "theme"> & {
20
+ /** props 就是 vditor 的 IOptions(theme 換成 VditorTheme,upload 多了 imageOnly),另外加 className、style */
21
+ export type VditorProps = Omit<IOptions, "theme" | "upload"> & {
22
22
  /** 預設取最近的 VditorThemeProvider,沒有就是 light */
23
23
  theme?: VditorTheme;
24
+ upload?: IUpload & {
25
+ /**
26
+ * 預設 true:upload 按鈕、拖曳、貼上只收圖片,accept 再從中限縮,按鈕提示為「上傳圖片」。
27
+ * false 時只依 accept 過濾,按鈕提示為「上傳圖片或文件」
28
+ */
29
+ imageOnly?: boolean;
30
+ };
24
31
  className?: string;
25
32
  style?: React.CSSProperties;
26
33
  };
@@ -41,8 +48,14 @@ export type VditorPreviewProps = Omit<Partial<IPreviewOptions>, "theme" | "mode"
41
48
  /** 只渲染 Markdown、不建立編輯器 */
42
49
  export declare const VditorPreview: (props: VditorPreviewProps) => React.ReactElement;
43
50
 
44
- /** 預覽用的 HTML 轉換:非 GFM 的區塊一律變成普通程式碼區塊 */
51
+ /** 非 GFM 的區塊(mermaid、數學式等)一律變成普通程式碼區塊 */
45
52
  export declare const gfmTransform: (html: string) => string;
46
53
 
47
- /** Markdown 轉 HTML,非 GFM 的區塊跟預覽一樣轉成普通程式碼區塊 */
54
+ /** 網址不合法的連結變成純文字,只留下連結文字 */
55
+ export declare const linkTransform: (html: string) => string;
56
+
57
+ /** 預覽輸出使用的轉換:依序套用 gfmTransform、linkTransform */
58
+ export declare const previewTransform: (html: string) => string;
59
+
60
+ /** Markdown 轉 HTML,跟預覽一樣套用 previewTransform */
48
61
  export declare const md2html: (markdown: string, options?: Partial<IPreviewOptions>) => Promise<string>;
package/dist/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import VditorCore from "vditor";
2
- import { defaultCdn } from "./cdn.js";
3
- import { gfmPreviewOptions, gfmTransform } from "./gfm.js";
2
+ import { previewOptions } from "./options/default.js";
4
3
  export { Vditor } from "./Vditor.js";
5
4
  export { VditorPreview } from "./VditorPreview.js";
6
5
  export { VditorThemeProvider } from "./theme.js";
7
- export { gfmTransform };
6
+ export { gfmTransform, linkTransform, previewTransform } from "./transforms/index.js";
8
7
 
9
- /** Markdown 轉 HTML,非 GFM 的區塊跟預覽一樣轉成普通程式碼區塊(原生 md2html 不吃 transform) */
8
+ /** Markdown 轉 HTML,跟預覽一樣套用 previewTransform(原生 md2html 不吃 transform) */
10
9
  export const md2html = async (markdown, options = {}) => {
11
- const merged = gfmPreviewOptions(options, options.cdn || defaultCdn());
10
+ const merged = previewOptions(options);
12
11
  return merged.transform(await VditorCore.md2html(markdown, merged));
13
12
  };
@@ -0,0 +1,81 @@
1
+ // 套件的預設選項:預設值合併 gfm.js 的 GFM 限定設定,再疊上使用端的選項
2
+ import { TOOLBAR, expandToolbar } from "../toolbar/index.js";
3
+ import { previewTransform } from "../transforms/index.js";
4
+ import { uploadableOnly } from "../upload.js";
5
+ import { merge } from "../utils/merge.js";
6
+ import { gfmEditorOptions, gfmPreviewOptions } from "./gfm.js";
7
+
8
+ /* global __webpack_public_path__ */
9
+ /**
10
+ * vditor 在執行期會去 `${cdn}/dist/...` 載入 lute、語系、圖示等資源。
11
+ * webpack plugin 把它們輸出在 output 目錄底下的 vditor/,所以預設 cdn 跟著 webpack 的 publicPath 走。
12
+ */
13
+ const defaultCdn = () => {
14
+ // 不是用 webpack 打包時,typeof 未宣告的識別字會得到 "undefined",不會拋錯
15
+ const base = typeof __webpack_public_path__ === "string" ? __webpack_public_path__ : "/";
16
+ return base === "" || base.endsWith("/") ? `${base}vditor` : `${base}/vditor`;
17
+ };
18
+
19
+ /** 使用端自己的 transform 接在 previewTransform 之後 */
20
+ const withPreviewTransform = transform => transform ? html => transform(previewTransform(html)) : previewTransform;
21
+
22
+ /** 編輯器選項 */
23
+ export const editorOptions = options => {
24
+ const defaults = merge({
25
+ cdn: defaultCdn(),
26
+ lang: "zh_TW",
27
+ mode: "wysiwyg",
28
+ // 傳 HTMLElement 給 vditor 時,開快取一定要有 cache.id,否則直接拋錯
29
+ cache: {
30
+ enable: false
31
+ },
32
+ toolbar: TOOLBAR,
33
+ preview: {
34
+ actions: []
35
+ },
36
+ // 本套件的選項:upload 按鈕、拖曳、貼上預設只收圖片
37
+ upload: {
38
+ imageOnly: true
39
+ }
40
+ }, gfmEditorOptions());
41
+ const merged = merge(defaults, options);
42
+ const {
43
+ upload
44
+ } = merged;
45
+ merged.upload = {
46
+ ...upload,
47
+ // upload 按鈕的檔案選擇框用 accept 決定列出哪些檔案;imageOnly 而沒指定 accept 時只列出圖片
48
+ accept: upload.imageOnly && !upload.accept ? "image/*" : upload.accept,
49
+ ...(upload.handler && {
50
+ handler: uploadableOnly(upload.handler, upload)
51
+ })
52
+ };
53
+ merged.preview = {
54
+ ...merged.preview,
55
+ transform: withPreviewTransform(options.preview?.transform),
56
+ // 內容主題不交給 vditor(它用整頁共用的 <link>),改由元件容器的 class 套用 index.css 裡局部化的主題
57
+ theme: {
58
+ ...merged.preview.theme,
59
+ current: ""
60
+ }
61
+ };
62
+ merged.toolbar = expandToolbar(merged.toolbar, merged.lang, upload.imageOnly);
63
+ return merged;
64
+ };
65
+
66
+ /** Vditor.preview 的選項 */
67
+ export const previewOptions = options => {
68
+ const defaults = merge({
69
+ cdn: defaultCdn(),
70
+ lang: "zh_TW",
71
+ mode: "light"
72
+ }, gfmPreviewOptions());
73
+ const merged = merge(defaults, options);
74
+ merged.transform = withPreviewTransform(options.transform);
75
+ // 同編輯器:內容主題改由元件容器的 class 決定
76
+ merged.theme = {
77
+ ...merged.theme,
78
+ current: ""
79
+ };
80
+ return merged;
81
+ };
@@ -0,0 +1,32 @@
1
+ // GFM 限定的設定:關掉 vditor 預設會渲染、但不屬於 GFM 的語法。
2
+ // 每次回傳新物件,避免不同的編輯器、預覽共用同一份選項物件
3
+
4
+ /** 編輯器 */
5
+ export const gfmEditorOptions = () => ({
6
+ preview: {
7
+ markdown: {
8
+ footnotes: false,
9
+ // GFM 以外、預設開著的只有註腳
10
+ codeBlockPreview: false,
11
+ // ir / wysiwyg 的程式碼區塊不開預覽面板,否則會去渲染 mermaid 等區塊
12
+ mathBlockPreview: false // 同上,$$ 區塊
13
+ },
14
+ render: {
15
+ media: {
16
+ enable: false
17
+ }
18
+ } // 不把影片、音訊連結嵌成 iframe
19
+ }
20
+ });
21
+
22
+ /** Vditor.preview */
23
+ export const gfmPreviewOptions = () => ({
24
+ markdown: {
25
+ footnotes: false
26
+ },
27
+ render: {
28
+ media: {
29
+ enable: false
30
+ }
31
+ }
32
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 預設工具列。跟 vditor 原本的相比:
3
+ * - 拿掉 export(寫死連 CDN)、devtools(要 echarts)、info(寫死 unpkg 的 logo)
4
+ * - 拿掉 emoji、record、code-theme、content-theme、help
5
+ */
6
+ export const TOOLBAR = ["headings", "bold", "italic", "strike", "link", "|", "list", "ordered-list", "check", "outdent", "indent", "|", "quote", "line", "code", "inline-code", "insert-before", "insert-after", "|", "upload", "table", "|", "undo", "redo", "|", "fullscreen", "edit-mode", {
7
+ name: "more",
8
+ toolbar: ["both", "outline", "preview"]
9
+ }];
10
+
11
+ /** upload.imageOnly 時 upload 按鈕的提示。vditor 語系檔沒有這個字,照各語系「上傳圖片或文件」的譯法拿掉「或文件」;沒列到的語言用英文 */
12
+ const IMAGE_ONLY_TIPS = {
13
+ en_US: "Upload image",
14
+ zh_CN: "上传图片",
15
+ zh_TW: "上傳圖片"
16
+ };
17
+
18
+ /**
19
+ * upload.imageOnly 時,upload 按鈕(含 more 之類的子選單裡的)的提示改成「上傳圖片」;使用端在物件裡給的 tip 優先。
20
+ * 不是 imageOnly 時沿用 vditor 語系檔的「上傳圖片或文件」
21
+ */
22
+ export const expandToolbar = (toolbar, lang, imageOnly) => toolbar.map(item => {
23
+ if (imageOnly && (item === "upload" || item?.name === "upload")) {
24
+ return {
25
+ name: "upload",
26
+ tip: IMAGE_ONLY_TIPS[lang] || IMAGE_ONLY_TIPS.en_US,
27
+ ...(typeof item === "string" ? {} : item)
28
+ };
29
+ }
30
+ return item?.toolbar ? {
31
+ ...item,
32
+ toolbar: expandToolbar(item.toolbar, lang, imageOnly)
33
+ } : item;
34
+ });
@@ -0,0 +1,14 @@
1
+ // 非 GFM 的區塊(mermaid、數學式、echarts…)一律變成普通程式碼區塊;webpack plugin 也不會輸出那些渲染套件。
2
+
3
+ /** Lute 會把這些語言輸出成 <div class="language-xxx"> 等渲染器畫圖 */
4
+ const DIV_LANGUAGES = "mermaid|echarts|abc|graphviz|mindmap|flowchart|plantuml|infographic|math";
5
+ /** 這些語言輸出成 <pre><code class="language-xxx">,但渲染器一樣會找上門 */
6
+ const CODE_LANGUAGES = "smiles|wavedrom|markmap|math";
7
+ const DIV_PATTERN = new RegExp(`<div(?: data-code="[^"]*")? class="language-(${DIV_LANGUAGES})">([\\s\\S]*?)</div>`, "g");
8
+ const CODE_PATTERN = new RegExp(`<code class="language-(${CODE_LANGUAGES})">`, "g");
9
+ const INLINE_MATH_PATTERN = /<span class="language-math">([\s\S]*?)<\/span>/g;
10
+
11
+ /**
12
+ * class 改成 language-plaintext,渲染器就找不到它們,也就不會去載入沒輸出的套件;原本的語言留在 data-lang。
13
+ */
14
+ export const gfmTransform = html => html.replace(DIV_PATTERN, '<pre><code class="language-plaintext" data-lang="$1">$2</code></pre>').replace(CODE_PATTERN, '<code class="language-plaintext" data-lang="$1">').replace(INLINE_MATH_PATTERN, "$$$1$$");
@@ -0,0 +1,7 @@
1
+ // 預覽輸出的 HTML 轉換:各個 transform 都從這裡引用
2
+ import { gfmTransform } from "./gfm.js";
3
+ import { linkTransform } from "./link.js";
4
+ export { gfmTransform, linkTransform };
5
+
6
+ /** 預覽輸出使用的轉換:非 GFM 的區塊變成普通程式碼區塊,再把網址不合法的連結變成純文字 */
7
+ export const previewTransform = html => linkTransform(gfmTransform(html));
@@ -0,0 +1,25 @@
1
+ // 網址不合法的連結變成純文字:拿掉 <a>,只留下連結文字(含粗體等格式)。
2
+ // Lute 的 sanitize 只會清掉 javascript: 開頭的 href,data:、vbscript: 等仍會輸出成連結,所以這裡再檢查一次。
3
+
4
+ // 規則取自 DOMPurify 的 src/regexp.ts:放行 http(s)、ftp(s)、mailto、tel 等協定,以及相對路徑
5
+ const IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
6
+ const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
7
+ const isAllowedUri = url => IS_ALLOWED_URI.test(url.replace(ATTR_WHITESPACE, ""));
8
+
9
+ /**
10
+ * 用 <template> 解析,裡面的 script 不會執行、圖片也不會載入。
11
+ * 沒有要拆的連結時原樣回傳:重新序列化會改變輸出的寫法(例如 <img … /> 變成 <img …>)
12
+ */
13
+ export const linkTransform = html => {
14
+ const template = document.createElement("template");
15
+ template.innerHTML = html;
16
+ let changed = false;
17
+ template.content.querySelectorAll("a").forEach(a => {
18
+ const href = a.getAttribute("href");
19
+ if (href === null || !isAllowedUri(href)) {
20
+ a.replaceWith(...a.childNodes);
21
+ changed = true;
22
+ }
23
+ });
24
+ return changed ? template.innerHTML : html;
25
+ };
package/dist/upload.js ADDED
@@ -0,0 +1,32 @@
1
+ // 上傳檔案的過濾。設了 upload.handler 時,vditor 不檢查檔案類型就把所有檔案交給 handler
2
+ // (拖曳、貼上、工具列的 upload 按鈕都是),這裡依 upload.imageOnly 與 upload.accept 補上檢查。
3
+
4
+ /**
5
+ * 檔案是否符合 accept,規則同 <input accept>:.png 比對副檔名、image/* 比對類型前綴、image/png 比對完整類型。
6
+ * accept 為空時不限制
7
+ */
8
+ const isAccepted = (file, accept) => !accept || accept.split(",").some(item => {
9
+ const type = item.trim().toLowerCase();
10
+ if (type.startsWith(".")) {
11
+ return file.name.toLowerCase().endsWith(type);
12
+ }
13
+ if (type.endsWith("/*")) {
14
+ return file.type.startsWith(type.slice(0, -1));
15
+ }
16
+ return file.type === type;
17
+ });
18
+
19
+ /** imageOnly 時必須是圖片,accept 再從中限縮;否則只看 accept */
20
+ const isUploadable = (file, {
21
+ imageOnly,
22
+ accept
23
+ }) => (!imageOnly || isAccepted(file, "image/*")) && isAccepted(file, accept);
24
+
25
+ /**
26
+ * 包裝 upload.handler:只把可上傳的檔案交給它。
27
+ * 一個都不符合時回傳提示文字,vditor 會顯示出來;不帶檔名,因為 vditor 用 innerHTML 顯示提示
28
+ */
29
+ export const uploadableOnly = (handler, upload) => files => {
30
+ const uploadable = files.filter(file => isUploadable(file, upload));
31
+ return uploadable.length > 0 ? handler(uploadable) : window.VditorI18n.fileTypeError;
32
+ };
@@ -0,0 +1,15 @@
1
+ const isPlainObject = value => Object.prototype.toString.call(value) === "[object Object]";
2
+
3
+ /** 物件逐層合併,陣列與其他值直接覆蓋 */
4
+ export const merge = (base, override) => {
5
+ if (!isPlainObject(base) || !isPlainObject(override)) {
6
+ return override === undefined ? base : override;
7
+ }
8
+ const result = {
9
+ ...base
10
+ };
11
+ Object.keys(override).forEach(key => {
12
+ result[key] = merge(result[key], override[key]);
13
+ });
14
+ return result;
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyttime04/react-vditor",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "description": "React component for the Vditor Markdown editor, limited to GFM and fully offline",
5
5
  "keywords": [
6
6
  "react",
package/dist/cdn.js DELETED
@@ -1,9 +0,0 @@
1
- // vditor 在執行期會去 `${cdn}/dist/...` 載入 lute、語系、圖示等資源。
2
- // webpack plugin 把它們輸出在 output 目錄底下的 vditor/,所以預設 cdn 跟著 webpack 的 publicPath 走。
3
-
4
- /* global __webpack_public_path__ */
5
- export const defaultCdn = () => {
6
- // 不是用 webpack 打包時,typeof 未宣告的識別字會得到 "undefined",不會拋錯
7
- const base = typeof __webpack_public_path__ === "string" ? __webpack_public_path__ : "/";
8
- return base === "" || base.endsWith("/") ? `${base}vditor` : `${base}/vditor`;
9
- };
package/dist/gfm.js DELETED
@@ -1,121 +0,0 @@
1
- // 只支援 GFM 的預設設定。vditor 原本的附加渲染(mermaid、數學式、echarts…)一律不渲染,
2
- // 顯示成普通程式碼區塊;webpack plugin 也不會輸出那些渲染套件。
3
- import { UPLOAD_IMAGE, uploadImageItem } from "./uploadImage.js";
4
-
5
- /** Lute 會把這些語言輸出成 <div class="language-xxx"> 等渲染器畫圖 */
6
- const DIV_LANGUAGES = "mermaid|echarts|abc|graphviz|mindmap|flowchart|plantuml|infographic|math";
7
- /** 這些語言輸出成 <pre><code class="language-xxx">,但渲染器一樣會找上門 */
8
- const CODE_LANGUAGES = "smiles|wavedrom|markmap|math";
9
- const DIV_PATTERN = new RegExp(`<div(?: data-code="[^"]*")? class="language-(${DIV_LANGUAGES})">([\\s\\S]*?)</div>`, "g");
10
- const CODE_PATTERN = new RegExp(`<code class="language-(${CODE_LANGUAGES})">`, "g");
11
- const INLINE_MATH_PATTERN = /<span class="language-math">([\s\S]*?)<\/span>/g;
12
-
13
- /**
14
- * 預覽用的 HTML 轉換:非 GFM 的區塊一律變成普通程式碼區塊。
15
- * class 改成 language-plaintext,渲染器就找不到它們,也就不會去載入沒輸出的套件;原本的語言留在 data-lang。
16
- */
17
- export const gfmTransform = html => html.replace(DIV_PATTERN, '<pre><code class="language-plaintext" data-lang="$1">$2</code></pre>').replace(CODE_PATTERN, '<code class="language-plaintext" data-lang="$1">').replace(INLINE_MATH_PATTERN, "$$$1$$");
18
-
19
- /**
20
- * 預設工具列。跟 vditor 原本的相比:
21
- * - 拿掉 export(寫死連 CDN)、devtools(要 echarts)、info(寫死 unpkg 的 logo)
22
- * - 拿掉 emoji、record、upload、code-theme、content-theme、help
23
- * - 加上只收圖片的 upload-image
24
- */
25
- const TOOLBAR = ["headings", "bold", "italic", "strike", "link", "|", "list", "ordered-list", "check", "outdent", "indent", "|", "quote", "line", "code", "inline-code", "insert-before", "insert-after", "|", UPLOAD_IMAGE, "table", "|", "undo", "redo", "|", "fullscreen", "edit-mode", {
26
- name: "more",
27
- toolbar: ["both", "outline", "preview"]
28
- }];
29
-
30
- /** 工具列裡的 upload-image(含 more 之類的子選單)換成完整的按鈕設定 */
31
- const expandToolbar = (toolbar, lang) => toolbar.map(item => {
32
- if (item === UPLOAD_IMAGE || item?.name === UPLOAD_IMAGE) {
33
- return uploadImageItem(item, lang);
34
- }
35
- return item?.toolbar ? {
36
- ...item,
37
- toolbar: expandToolbar(item.toolbar, lang)
38
- } : item;
39
- });
40
- const isPlainObject = value => Object.prototype.toString.call(value) === "[object Object]";
41
-
42
- /** 物件逐層合併,陣列與其他值直接覆蓋 */
43
- const merge = (base, override) => {
44
- if (!isPlainObject(base) || !isPlainObject(override)) {
45
- return override === undefined ? base : override;
46
- }
47
- const result = {
48
- ...base
49
- };
50
- Object.keys(override).forEach(key => {
51
- result[key] = merge(result[key], override[key]);
52
- });
53
- return result;
54
- };
55
-
56
- /** 使用端自己的 transform 接在 gfmTransform 之後 */
57
- const withGfmTransform = transform => transform ? html => transform(gfmTransform(html)) : gfmTransform;
58
-
59
- /** 編輯器選項:GFM 預設值,再疊上使用端的選項 */
60
- export const gfmEditorOptions = (options, cdn) => {
61
- const merged = merge({
62
- cdn,
63
- lang: "zh_TW",
64
- mode: "wysiwyg",
65
- // 傳 HTMLElement 給 vditor 時,開快取一定要有 cache.id,否則直接拋錯
66
- cache: {
67
- enable: false
68
- },
69
- toolbar: TOOLBAR,
70
- preview: {
71
- actions: [],
72
- markdown: {
73
- footnotes: false,
74
- // GFM 以外、預設開著的只有註腳
75
- codeBlockPreview: false,
76
- // ir / wysiwyg 的程式碼區塊不開預覽面板,否則會去渲染 mermaid 等區塊
77
- mathBlockPreview: false // 同上,$$ 區塊
78
- },
79
- render: {
80
- media: {
81
- enable: false
82
- }
83
- } // 不把影片、音訊連結嵌成 iframe
84
- }
85
- }, options);
86
- merged.preview = {
87
- ...merged.preview,
88
- transform: withGfmTransform(options.preview?.transform),
89
- // 內容主題不交給 vditor(它用整頁共用的 <link>),改由元件容器的 class 套用 index.css 裡局部化的主題
90
- theme: {
91
- ...merged.preview.theme,
92
- current: ""
93
- }
94
- };
95
- merged.toolbar = expandToolbar(merged.toolbar, merged.lang);
96
- return merged;
97
- };
98
-
99
- /** Vditor.preview 的選項:GFM 預設值,再疊上使用端的選項 */
100
- export const gfmPreviewOptions = (options, cdn) => {
101
- const merged = merge({
102
- cdn,
103
- lang: "zh_TW",
104
- mode: "light",
105
- markdown: {
106
- footnotes: false
107
- },
108
- render: {
109
- media: {
110
- enable: false
111
- }
112
- }
113
- }, options);
114
- merged.transform = withGfmTransform(options.transform);
115
- // 同編輯器:內容主題改由元件容器的 class 決定
116
- merged.theme = {
117
- ...merged.theme,
118
- current: ""
119
- };
120
- return merged;
121
- };
@@ -1,51 +0,0 @@
1
- // 「上傳圖片」按鈕:toolbar 裡寫 "upload-image" 就會換成這顆按鈕。
2
- // vditor 內建的 upload 只有一顆,檔案類型看的是共用的 upload.accept,所以另做一顆只收圖片的。
3
-
4
- /** 按鈕的 tip。vditor 語系檔沒有這個字,照各語系「上傳圖片或文件」的譯法拿掉「或文件」;沒列到的語言用英文 */
5
- const TIPS = {
6
- en_US: "Upload image",
7
- zh_CN: "上传图片",
8
- zh_TW: "上傳圖片"
9
- };
10
- export const UPLOAD_IMAGE = "upload-image";
11
-
12
- /**
13
- * 選完圖片後交給 upload.handler,做法與 vditor 內建上傳在設了 handler 時相同:
14
- * upload.multiple 不是 true 時只能選一張;handler 回傳字串時當成錯誤訊息顯示。
15
- * 只支援 upload.handler,不支援 upload.url(那條路是 vditor 內部的 XHR 上傳流程,沒有公開的 API 可以呼叫)。
16
- */
17
- const pickImages = vditor => {
18
- const {
19
- handler,
20
- multiple
21
- } = vditor.options.upload;
22
- if (!handler) {
23
- vditor.tip.show("please config: options.upload.handler");
24
- return;
25
- }
26
- const input = document.createElement("input");
27
- input.type = "file";
28
- input.accept = "image/*";
29
- input.multiple = multiple === true;
30
- input.addEventListener("change", async () => {
31
- // 檔案選擇框可以切成「所有檔案」,所以再過濾一次
32
- const images = [...input.files].filter(file => file.type.startsWith("image/"));
33
- if (images.length === 0) {
34
- return;
35
- }
36
- const message = await handler(images);
37
- if (typeof message === "string") {
38
- vditor.tip.show(message);
39
- }
40
- });
41
- input.click();
42
- };
43
-
44
- /** "upload-image"(字串或物件)換成完整的按鈕設定;使用端在物件裡給的欄位優先 */
45
- export const uploadImageItem = (item, lang) => ({
46
- name: UPLOAD_IMAGE,
47
- icon: '<svg><use xlink:href="#vditor-icon-upload"></use></svg>',
48
- tip: TIPS[lang] || TIPS.en_US,
49
- click: (event, vditor) => pickImages(vditor),
50
- ...(typeof item === "string" ? {} : item)
51
- });