diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml new file mode 100644 index 00000000..f4651ed4 --- /dev/null +++ b/.github/workflows/pr-merge.yml @@ -0,0 +1,79 @@ +name: PR Closed + +on: + pull_request_target: + types: + - closed + +jobs: + remove_assets: + runs-on: ubuntu-latest + + steps: + # 检出仓库代码 + - name: Checkout repository + uses: actions/checkout@v2 + + # 打印 PR 详细信息 + - name: Print PR details + run: | + echo "The PR ID is ${{ github.event.pull_request.id }}" + echo "The PR number is ${{ github.event.pull_request.number }}" + echo "The PR title is ${{ github.event.pull_request.title }}" + echo "The PR branch is ${{ github.event.pull_request.head.ref }}" + + # 安装 cos-nodejs-sdk-v5 + - name: Install cos-nodejs-sdk-v5 + run: npm install cos-nodejs-sdk-v5 + + # 删除对应的资源 + - name: Delete Resources On COS + uses: actions/github-script@v7 + with: + script: | + const COS = require('cos-nodejs-sdk-v5'); + + const cos = new COS({ + SecretId: process.env.COS_SECRETID, + SecretKey: process.env.COS_SECRETKEY, + }); + + const bucket = 'cherrymd-1301618266'; + const region = 'ap-singapore'; + const prNumber = '${{ github.event.pull_request.number }}'; + const prefix = `pr${prNumber}/`; + + // List objects in the bucket with the specified prefix + cos.getBucket({ + Bucket: bucket, + Region: region, + Prefix: prefix, + }, (err, data) => { + if (err) { + console.error('Error listing objects:', err); + return; + } + + const objectsToDelete = data.Contents.map(item => ({ Key: item.Key })); + + if (objectsToDelete.length === 0) { + console.log('No objects to delete.'); + return; + } + + // Delete the listed objects + cos.deleteMultipleObject({ + Bucket: bucket, + Region: region, + Objects: objectsToDelete, + }, (err, data) => { + if (err) { + console.error('Error deleting objects:', err); + } else { + console.log('Successfully deleted objects:', data); + } + }); + }); + env: + COS_SECRETID: ${{ secrets.COS_SECRETID }} + COS_SECRETKEY: ${{ secrets.COS_SECRETKEY }} diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index e3a87db1..9d8bf7a5 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -6,6 +6,8 @@ on: jobs: build: + # 不需要在fork仓库的pr中运行 + if: github.repository == 'Tencent/cherry-markdown' runs-on: ubuntu-latest env: BASE_SHA: ${{ github.event.pull_request.base.sha }} diff --git a/README.JP.md b/README.JP.md new file mode 100644 index 00000000..38a31cda --- /dev/null +++ b/README.JP.md @@ -0,0 +1,294 @@ +

cherry logo

+ +# Cherry Markdown Writer + +[![Cloud Studio Template](https://cs-res.codehub.cn/common/assets/icon-badge.svg)](https://cloudstudio.net#https://github.com/Tencent/cherry-markdown) + +日本語 | [English](./README.md) | [简体中文](./README.CN.md) + +### ドキュメント +- [初識cherry markdown 編集器](https://github.com/Tencent/cherry-markdown/wiki/%E5%88%9D%E8%AF%86cherry-markdown-%E7%BC%96%E8%BE%91%E5%99%A8) +- [hello world](https://github.com/Tencent/cherry-markdown/wiki/hello-world) +- [画像&ファイルアップロードインターフェースの設定](https://github.com/Tencent/cherry-markdown/wiki/%E9%85%8D%E7%BD%AE%E5%9B%BE%E7%89%87&%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%8E%A5%E5%8F%A3) +- [ツールバーの調整](https://github.com/Tencent/cherry-markdown/wiki/%E8%B0%83%E6%95%B4%E5%B7%A5%E5%85%B7%E6%A0%8F) +- [カスタムシンタックス](https://github.com/Tencent/cherry-markdown/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E8%AF%AD%E6%B3%95) +- [設定項目の全解](https://github.com/Tencent/cherry-markdown/wiki/%E9%85%8D%E7%BD%AE%E9%A1%B9%E5%85%A8%E8%A7%A3) +- [テーマの設定](https://github.com/Tencent/cherry-markdown/wiki/%E9%85%8D%E7%BD%AE%E4%B8%BB%E9%A2%98) +- [コードブロックシンタックスの拡張](https://github.com/Tencent/cherry-markdown/wiki/%E6%89%A9%E5%B1%95%E4%BB%A3%E7%A0%81%E5%9D%97%E8%AF%AD%E6%B3%95) +- [イベント&コールバック](https://github.com/Tencent/cherry-markdown/wiki/%E4%BA%8B%E4%BB%B6&%E5%9B%9E%E8%B0%83) +- [API](https://tencent.github.io/cherry-markdown/examples/api.html) + +### デモ + +- [フルモデル](https://tencent.github.io/cherry-markdown/examples/index.html) +- [ベーシック](https://tencent.github.io/cherry-markdown/examples/basic.html) +- [モバイル](https://tencent.github.io/cherry-markdown/examples/h5.html) +- [複数インスタンス](https://tencent.github.io/cherry-markdown/examples/multiple.html) +- [ツールバーなしエディタ](https://tencent.github.io/cherry-markdown/examples/notoolbar.html) +- [純プレビュー](https://tencent.github.io/cherry-markdown/examples/preview_only.html) +- [XSS](https://tencent.github.io/cherry-markdown/examples/xss.html)(デフォルトでは許可されていません) +- [画像WYSIWYG](https://tencent.github.io/cherry-markdown/examples/img.html) +- [テーブルWYSIWYG](https://tencent.github.io/cherry-markdown/examples/table.html) +- [自動番号付きヘッダー](https://tencent.github.io/cherry-markdown/examples/head_num.html) +- [ストリーム入力モード(AIチャートシナリオ)](https://tencent.github.io/cherry-markdown/examples/ai_chat.html) + +----- + +## 紹介 + +Cherry Markdown Editorは、Javascriptで書かれたMarkdownエディタです。Cherry Markdown Editorは、すぐに使える、軽量でシンプル、拡張が容易などの利点があります。ブラウザやサーバー(NodeJs)で動作します。 + +### **すぐに使える** + +開発者は非常に簡単な方法でCherry Markdown Editorを呼び出してインスタンス化できます。インスタンス化されたエディタは、デフォルトでほとんどの一般的なMarkdownシンタックス(タイトル、目次、フローチャート、数式など)をサポートします。 + +### **拡張が容易** + +Cherry Markdown Editorがサポートするシンタックスが開発者のニーズを満たさない場合、迅速に二次開発や機能拡張を行うことができます。同時に、Cherry Markdown Editorは純粋なJavaScriptで実装されるべきであり、angular、vue、reactなどのフレームワーク技術に依存すべきではありません。フレームワークはコンテナ環境を提供するだけです。 + +## 特徴 + +### シンタックスの特徴 + +1. 画像のズーム、配置、引用 +2. テーブルの内容に基づいてチャートを生成 +3. フォントの色とサイズの調整 +4. フォントの背景色、上付き文字、下付き文字 +5. チェックリストの挿入 +6. オーディオやビデオの挿入 + +### 複数のモード + +1. スクロール同期付きのライブプレビュー +2. プレビューのみのモード +3. ツールバーなしのモード(ミニマリスト編集モード) +4. モバイルプレビューモード + +### 機能の特徴 + +1. リッチテキストからコピーしてMarkdownテキストとして貼り付け +2. クラシック改行&通常改行 +3. マルチカーソル編集 +4. 画像サイズの編集 +5. 画像やPDFとしてエクスポート +6. 新しい行の先頭に表示されるフロートツールバー +7. テキストを選択すると表示されるバブルツールバー + +### パフォーマンスの特徴 + +1. 部分的なレンダリング +2. 部分的な更新 + +### セキュリティ + +Cherry Markdownには組み込みのセキュリティフックがあり、ホワイトリストのフィルタリングとDomPurifyを使用してスキャンとフィルタリングを行います。 + +### スタイルテーマ + +Cherry Markdownにはさまざまなスタイルテーマが用意されています。 + +### 特徴の表示 + +詳細については[こちら](https://github.com/Tencent/cherry-markdown/wiki/%E7%89%B9%E6%80%A7%E5%B1%95%E7%A4%BA-features)をクリックしてください。 + +## インストール + +yarnを使用 + +```bash +yarn add cherry-markdown +``` + +npmを使用 + +```bash +npm install cherry-markdown --save +``` + +`mermaid`描画とテーブル自動チャート機能を有効にする必要がある場合は、`mermaid`と`echarts`パッケージを同時に追加する必要があります。 + +現在、**Cherry**が推奨するプラグインバージョンは`echarts@4.6.0`、`mermaid@9.4.3`です。 + +```bash +# mermaid依存関係をインストールしてmermaid描画機能を有効にする +yarn add mermaid@9.4.3 +# echarts依存関係をインストールしてテーブル自動チャート機能を有効にする +yarn add echarts@4.6.0 +``` + +## クイックスタート + +### ブラウザ + +#### UMD + +```html + +
+ + +``` + +#### ESM + +```javascript +import 'cherry-markdown/dist/cherry-markdown.css'; +import Cherry from 'cherry-markdown'; +const cherryInstance = new Cherry({ + id: 'markdown-container', + value: '# welcome to cherry editor!', +}); +``` + +### Node + +```javascript +const { default: CherryEngine } = require('cherry-markdown/dist/cherry-markdown.engine.core.common'); +const cherryEngineInstance = new CherryEngine(); +const htmlContent = cherryEngineInstance.makeHtml('# welcome to cherry editor!'); +``` + +## ライトバージョンの使用 + +mermaidライブラリのサイズが非常に大きいため、cherryビルド製品にはmermaidを内蔵しないコアビルドパッケージが含まれています。コアビルドは以下の方法でインポートできます。 + +### フルモード(UIインターフェース付き) + +```javascript +import 'cherry-markdown/dist/cherry-markdown.css'; +import Cherry from 'cherry-markdown/dist/cherry-markdown.core'; +const cherryInstance = new Cherry({ + id: 'markdown-container', + value: '# welcome to cherry editor!', +}); +``` + +### エンジンモード(シンタックスコンパイルのみ) + +```javascript +// Cherryエンジンコアビルドをインポート +// エンジンの設定項目はCherryの設定項目と同じです。以下のドキュメント内容はCherryコアパッケージのみを紹介します。 +import CherryEngine from 'cherry-markdown/dist/cherry-markdown.engine.core'; +const cherryEngineInstance = new CherryEngine(); +const htmlContent = cherryEngineInstance.makeHtml('# welcome to cherry editor!'); + +// -->

welcome to cherry editor!

+``` + +### ⚠️ mermaidについて + +コアビルドパッケージにはmermaid依存関係が含まれていないため、関連プラグインを手動でインポートする必要があります。 + +```javascript +import 'cherry-markdown/dist/cherry-markdown.css'; +import Cherry from 'cherry-markdown/dist/cherry-markdown.core'; +import CherryMermaidPlugin from 'cherry-markdown/dist/addons/cherry-code-block-mermaid-plugin'; +import mermaid from 'mermaid'; + +// プラグインの登録はCherryのインスタンス化前に行う必要があります +Cherry.usePlugin(CherryMermaidPlugin, { + mermaid, // mermaidオブジェクトを渡す + // mermaidAPI: mermaid.mermaidAPI, // mermaid APIを渡すこともできます + // 同時にここでmermaidの動作を設定できます。詳細はmermaid公式ドキュメントを参照してください。 + // theme: 'neutral', + // sequence: { useMaxWidth: false, showSequenceNumbers: true } +}); + +const cherryInstance = new Cherry({ + id: 'markdown-container', + value: '# welcome to cherry editor!', +}); +``` + +### 動的インポート + +**推奨** 動的インポートを使用します。以下はwebpackの動的インポートの例です。 + +```javascript +import 'cherry-markdown/dist/cherry-markdown.css'; +import Cherry from 'cherry-markdown/dist/cherry-markdown.core'; + +const registerPlugin = async () => { + const [{ default: CherryMermaidPlugin }, mermaid] = await Promise.all([ + import('cherry-markdown/src/addons/cherry-code-block-mermaid-plugin'), + import('mermaid'), + ]); + Cherry.usePlugin(CherryMermaidPlugin, { + mermaid, // mermaidオブジェクトを渡す + }); +}; + +registerPlugin().then(() => { + // プラグインの登録はCherryのインスタンス化前に行う必要があります + const cherryInstance = new Cherry({ + id: 'markdown-container', + value: '# welcome to cherry editor!', + }); +}); +``` + +## 設定 +`/src/Cherry.config.js`を参照するか、[こちら](https://github.com/Tencent/cherry-markdown/wiki/%E9%85%8D%E7%BD%AE%E9%A1%B9%E5%85%A8%E8%A7%A3)をクリックしてください。 + +## 例 + +詳細な例については[こちら](https://github.com/Tencent/cherry-markdown/wiki)をクリックしてください。 + +### クライアント +開発中です。詳細は`/client/`ディレクトリを参照してください。 + +## 拡張 + +### カスタムシンタックス +[こちら](https://github.com/Tencent/cherry-markdown/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E8%AF%AD%E6%B3%95)をクリックしてください。 + +### カスタムツールバー +[こちら](https://github.com/Tencent/cherry-markdown/wiki/%E8%B0%83%E6%95%B4%E5%B7%A5%E5%85%B7%E6%A0%8F)をクリックしてください。 + +## ユニットテスト + +Jestはそのアサーション、非同期サポート、スナップショット機能のために選ばれました。ユニットテストにはCommonMarkテストとスナップショットテストが含まれます。 + +### CommonMarkテスト + +`yarn run test:commonmark`を実行して公式のCommonMarkスイートをテストします。このコマンドは高速で実行されます。 + +スイートは`test/suites/commonmark.spec.json`にあります。例えば: + +```json +{ + "markdown": " \tfoo\tbaz\t\tbim\n", + "html": "
foo\tbaz\t\tbim\n
\n", + "example": 2, + "start_line": 363, + "end_line": 368, + "section": "Tabs" +}, +``` + +この場合、Jestは`Cherry.makeHtml(" \tfoo\tbaz\t\tbim\n")`によって生成されたHTMLを期待される結果`"
foo\tbaz\t \tbim\n
\n"`と比較します。Cherry Markdownのマッチャーは`data-line`などのプライベート属性を無視しています。 + +CommonMarkの仕様とスイートは次の場所から取得できます:https://spec.commonmark.org/ 。 + +### スナップショットテスト + +`yarn run test:snapshot`を実行してスナップショットテストを実行します。`test/core/hooks/List.spec.ts`のようにスナップショットスイートを書くことができます。最初の実行時にスナップショットが自動的に生成されます。その後、Jestはスナップショットと生成されたHTMLを比較できます。スナップショットを再生成する必要がある場合は、`test/core/hooks/__snapshots__`の下にある古いスナップショットを削除してこのコマンドを再度実行します。 + +スナップショットテストは遅く実行されます。エラーが発生しやすく、Cherry Markdownの特別なシンタックスを含むフックをテストするためにのみ使用されるべきです。 + +## 貢献 + +より強力なMarkdownエディタを構築するために参加してください。もちろん、機能リクエストを提出することもできます。作業を始める前に[こちら](https://github.com/Tencent/cherry-markdown/wiki/%E5%88%9D%E8%AF%86cherry-markdown-%E7%BC%96%E8%BE%91%E5%99%A8)を読んでください。 + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/Tencent/cherry-markdown.svg)](https://starchart.cc/Tencent/cherry-markdown) + +## ライセンス + +Apache-2.0 diff --git a/README.md b/README.md index 567614ca..123c8dad 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Cloud Studio Template](https://cs-res.codehub.cn/common/assets/icon-badge.svg)](https://cloudstudio.net#https://github.com/Tencent/cherry-markdown) -English | [简体中文](./README.CN.md) +English | [简体中文](./README.CN.md) | [日本語](./README.JP.md) ### Document - [初识cherry markdown 编辑器](https://github.com/Tencent/cherry-markdown/wiki/%E5%88%9D%E8%AF%86cherry-markdown-%E7%BC%96%E8%BE%91%E5%99%A8) diff --git a/examples/api.html b/examples/api.html index 12f5a589..31989e9b 100644 --- a/examples/api.html +++ b/examples/api.html @@ -270,6 +270,56 @@

toolbar.toolbarHandlers.italic()

+
+

toolbar.toolbarHandlers.strikethrough()

+

向cherry编辑器中插入删除线语法

+
+ + 试一试 +
+
+ +
+

toolbar.toolbarHandlers.size(fontSize: int)

+

向cherry编辑器中插入字体大小语法

+
+ + 试一试 +
+
+ +
+

toolbar.toolbarHandlers.color(param:string)

+

+ 向cherry编辑器中插入字体颜色或字体背景色语法 + + + + + + + + + + + + + + + + + +
param效果
'color: #c2255c'字体颜色
'background-color: #c2255c'字体背景颜色
+

+
+ + 试一试 +
+
+

toolbar.toolbarHandlers.header(level:int)

向cherry编辑器中插入标题语法

@@ -283,11 +333,65 @@

toolbar.toolbarHandlers.header(level:int)

-

toolbar.toolbarHandlers.strikethrough()

-

向cherry编辑器中插入删除线语法

+

toolbar.toolbarHandlers.quote()

+

向cherry编辑器中插入引用语法

+
+ + 试一试 +
+
+ +
+

toolbar.toolbarHandlers.panel(param:string)

+

+ 向cherry编辑器中插入对齐方式或信息面板语法 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
param效果
'left'左对齐
'center'居中对齐
'right'右对齐
'primary'首选项
'info'一般信息
'warning'警告
'danger'危险
'success'成功
+

+cherryObj.toolbar.toolbarHandlers.panel('success') 试一试
diff --git a/examples/cherry-markdown-react-demo/.gitignore b/examples/cherry-markdown-react-demo/.gitignore new file mode 100644 index 00000000..4d29575d --- /dev/null +++ b/examples/cherry-markdown-react-demo/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/cherry-markdown-react-demo/README.md b/examples/cherry-markdown-react-demo/README.md new file mode 100644 index 00000000..a732af20 --- /dev/null +++ b/examples/cherry-markdown-react-demo/README.md @@ -0,0 +1,8 @@ +# 介绍 +这是cherry-markdown的react示例 + +# 开始 +``` +npm install +npm start +``` \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/package.json b/examples/cherry-markdown-react-demo/package.json new file mode 100644 index 00000000..6db4cf3c --- /dev/null +++ b/examples/cherry-markdown-react-demo/package.json @@ -0,0 +1,35 @@ +{ + "name": "cherry-markdown-react-demo", + "version": "0.1.0", + "private": true, + "dependencies": { + "cherry-markdown": "^0.8.44", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-scripts": "5.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/public/favicon.ico b/examples/cherry-markdown-react-demo/public/favicon.ico new file mode 100644 index 00000000..aa8c8987 Binary files /dev/null and b/examples/cherry-markdown-react-demo/public/favicon.ico differ diff --git a/examples/cherry-markdown-react-demo/public/github.svg b/examples/cherry-markdown-react-demo/public/github.svg new file mode 100644 index 00000000..eeb3811a --- /dev/null +++ b/examples/cherry-markdown-react-demo/public/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/public/index.html b/examples/cherry-markdown-react-demo/public/index.html new file mode 100644 index 00000000..d35cfde0 --- /dev/null +++ b/examples/cherry-markdown-react-demo/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + Cherry React Demo + + + + +
+ + + + \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/public/logo--color.png b/examples/cherry-markdown-react-demo/public/logo--color.png new file mode 100644 index 00000000..786b7a5b Binary files /dev/null and b/examples/cherry-markdown-react-demo/public/logo--color.png differ diff --git a/examples/cherry-markdown-react-demo/public/manifest.json b/examples/cherry-markdown-react-demo/public/manifest.json new file mode 100644 index 00000000..c4e4f94b --- /dev/null +++ b/examples/cherry-markdown-react-demo/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/App.css b/examples/cherry-markdown-react-demo/src/App.css new file mode 100644 index 00000000..7e566de8 --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/App.css @@ -0,0 +1,178 @@ +:root { + --width: 1400px; + --color: #e26862; +} + +header { + height: 60px; + border-bottom: 1px solid var(--color); + display: flex; + justify-content: center; +} + +h2 { + margin-top: 8px; + margin-bottom: 8px; +} + +h3 { + margin-top: 6px; + margin-bottom: 6px; +} + +.header-wrapper { + height: 100%; + width: var(--width); + display: flex; + justify-content: start; + align-items: center; +} + +.header-logo { + height: 60%; + width: auto; + object-fit: contain; +} + +.header-repo-a { + height: 100%; + display: flex; + align-items: center; + margin-left: auto; +} + +.header-text { + font-size: 20px; + text-decoration: none; + color: inherit; + padding: 10px; + + margin-left: 15px; + border-radius: 5px; +} + +.header-text:hover { + background-color: var(--color); + color: white; +} + +.header-text:nth-child(2) { + background-color: var(--color); + color: white; +} + +main { + display: flex; + align-items: center; + flex-direction: column; + width: 100%; +} + +.main-wrapper { + width: var(--width) +} + +.title-wrapper { + width: 100%; +} + +.title { + display: flex; + justify-content: start; +} + +.menu { + border: 1px solid gray; + padding: 8px; + border-radius: 5px; + margin-bottom: 10px; +} + + +#markdown-container { + width: 100%; + height: 800px !important; +} + +.custom-select-container { + display: inline-block; + min-width: 250px; + text-align: center; + position: relative; + z-index: 100; + margin-left: 5px; +} + +.custom-select { + padding: 5px; + border-bottom: 1px solid var(--color); +} + +.custom-option-wrapper { + position: absolute; + width: 100%; + margin: 0; + padding: 0; + text-align: center; + background: white; + border: 1px solid var(--color); + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} + +.custom-option { + list-style-type: none; + margin: 6px 10px; + border-bottom: 1px solid gainsboro; + cursor: pointer; + line-height: 30px; + border-radius: 5px; +} + +.custom-option:hover { + /* background-color: gray; */ + color: #ffffff; + background-color: var(--color); +} + + +.code-input { + width: 99%; + max-width: 99%; + padding: 5px; + font-size: 20px; +} + + +.demo-desc { + font-size: 17px; + white-space: pre-wrap; +} + + +.run-btn { + background-color: white; + font-size: 20px; + color: white; + border: 1px solid var(--color); + background-color: var(--color); + border-radius: 5px; + margin-left: 5px; + cursor: pointer; +} + +.run-btn:disabled { + background-color: gainsboro; + border: 1px solid gainsboro; + cursor: default; +} + +.run-btn:disabled:hover { + background-color: gainsboro; + border: 1px solid gainsboro; +} + +.run-btn:hover { + background-color: red; + border: 1px solid red; +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/App.jsx b/examples/cherry-markdown-react-demo/src/App.jsx new file mode 100644 index 00000000..04d749ca --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/App.jsx @@ -0,0 +1,40 @@ +import { useEffect, useState, useRef } from 'react'; +import './App.css'; +import Header from './components/Header' +import 'cherry-markdown/dist/cherry-markdown.css'; +import Cherry from 'cherry-markdown'; +import Title from './components/Title'; +import Menu from "./components/Menu"; + +function App() { + const editorRef = useRef(null); + const [editor, setEditor] = useState(null); + useEffect(() => { + if (editor == null) { + // 初始化编辑器 + const config = { + el: editorRef.current, + value: '', + callback: { + afterChange: (md, html) => console.log('change'), + }, + }; + setEditor(new Cherry(config)); + } + }, []); + return ( + <> +
+
+
+ + <Menu cherryObj={editor} /> + {/* 该div作为编辑器的最外层容器 */} + <div ref={editorRef} id="markdown-container" /> + </div> + </main> + </> + ); +} + +export default App; \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/components/Header.jsx b/examples/cherry-markdown-react-demo/src/components/Header.jsx new file mode 100644 index 00000000..18f9efcf --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/components/Header.jsx @@ -0,0 +1,16 @@ +export default () => { + return ( + <header> + <div className="header-wrapper"> + <img src="logo--color.png" className="header-logo" /> + <a href="#" className="header-text">Demo</a> + <a href="https://github.com/Tencent/cherry-markdown/wiki/%E5%88%9D%E8%AF%86cherry-markdown-%E7%BC%96%E8%BE%91%E5%99%A8" target="_blank" className="header-text">介绍</a> + <a href="https://tencent.github.io/cherry-markdown/examples/api.html" target="_blank" className="header-text">API文档</a> + <a href="https://tencent.github.io/cherry-markdown/examples/preview_only.html" target="_blank" className="header-text">Markdown语法</a> + <a href="https://github.com/Tencent/cherry-markdown" target="_black" className="header-repo-a"> + <img src="github.svg" className="header-logo" /> + </a> + </div> + </header> + ) +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/components/Menu.jsx b/examples/cherry-markdown-react-demo/src/components/Menu.jsx new file mode 100644 index 00000000..269512a5 --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/components/Menu.jsx @@ -0,0 +1,496 @@ +import { useEffect, useRef, useState } from "react" + +const Select = ({ options, onChange, selected }) => { + const [showOptions, setShowOptions] = useState(false); + return ( + <div className="custom-select-container"> + <div + className={showOptions ? "custom-select active" : "custom-select"} + onClick={() => { setShowOptions(prev => !prev); }} + > + {options[selected]} + </div> + {showOptions && ( + <ul className="custom-option-wrapper"> + {options.map((option, idx) => { + return ( + <li key={idx} + className="custom-option" + onClick={() => { onChange(idx); setShowOptions(false); }} + > + {option} + </li> + ) + })} + </ul> + )} + </div> + ); +} + +const getFullCode = (code) => `\`\`\`javascript +import { useEffect, useState, useRef } from 'react'; +import 'cherry-markdown/dist/cherry-markdown.css'; +import Cherry from 'cherry-markdown'; +export default () => { + const editorRef = useRef(null); + const [cherry, setCherry] = useState(null); + useEffect(() => { + if (cherry == null) { + // 初始化编辑器 + const config = { + el: editorRef.current, + value: '', + callback: { + afterChange: (md, html) => console.log('change'), + }, + }; + const cherryObj = new Cherry(config); + setCherry(cherryObj); + // api操作如下 + ${code} + } + },[]) + return (<div ref={editorRef} />); +} +\`\`\`` + +const data = [ + { + name: "Cherry API", api: [ + { + name: "setMarkdown", title: `setMarkdown(content:string, keepCursor = false)`, + desc: `设置内容,直接编辑器中的全部文本。setValue(content:string, keepCursor = false)有同样的功能,keepCursor = true 时更新内容的时候保持光标位置`, + code: `cherryObj.setMarkdown("初始内容"); +setTimeout(()=>{cherryObj.setMarkdown("2秒后替换成的新内容")},2000);`, + markdown: `# Cherry API +## setMarkdown(content:string, keepCursor = false) +设置内容 +${getFullCode(`cherryObj.setMarkdown("初始内容"); + setTimeout(()=>{cherryObj.setMarkdown("2秒后替换成的新内容")},2000);`)} + + ` + }, + { + name: "insert", + title: `insert(content: string, isSelect = false, anchor = false, focus = true)`, + desc: `插入内容\nisSelect = true 选中刚插入的内容\nanchor = false 在光标处插入内容\nanchor = [1, 3] 在第2行第4个字符处插入内容`, + code: `cherryObj.insert("在光标处插入内容"); +cherryObj.insert("在第二行插入内容,并选中插入的内容", true, [1, 0]);`, + markdown: `# Cherry API +## insert(content: string, isSelect = false, anchor = false, focus = true) +在光标处或者指定行 + 偏移量插入内容 + > insert(\`content\`, \`isSelect\`, \`anchor\`, \`focus\`) +- \`content\` 被插入的文本 +- \`isSelect\` 是否选中刚插入的内容,默认false,不选中 +- \`anchor\` [x,y] 代表x+1行,y+1字符偏移量,默认false 会从光标处插入 +- \`focus\` 保持编辑器处于focus状态,默认true,选中编辑器(用户可以继续输入) +${getFullCode(`cherryObj.insert("在光标处插入内容"); + cherryObj.insert("在第二行插入内容,并选中插入的内容", true, [1, 0]);`)}`, + }, + { + name: "getMarkdown", + title: `getMarkdown()`, + desc: `获取markdown内容`, + code: `alert(cherryObj.getMarkdown()); +console.log(cherryObj.getMarkdown());`, + markdown: `# Cherry API +## getMarkdown() +获取markdown内容 +${getFullCode(`alert(cherryObj.getMarkdown()); + console.log(cherryObj.getMarkdown());`)}`, + }, + { + name: "getHtml", + title: `getHtml()`, + desc: `获取渲染后的html内容`, + code: `alert(cherryObj.getHtml()); +console.log(cherryObj.getHtml());`, + markdown: `# Cherry API +## getHtml() +获取渲染后的html内容 +${getFullCode(`alert(cherryObj.getHtml()); + console.log(cherryObj.getHtml());`)}`, + }, + { + name: "destroy", + title: `destroy()`, + desc: `销毁函数`, + code: `cherryObj.destroy();`, + markdown: `# Cherry API +## destroy() +销毁函数 +${getFullCode(`// cherryObj.destroy(); `)}`, + }, + { + name: "resetToolbar", + title: `resetToolbar(type:string, toolbar:array)`, + desc: `重置工具栏 +type 修改工具栏的类型 {'toolbar'|'toolbarRight'|'sidebar'|'bubble'|'float'} +toolbar 工具栏配置`, + code: `cherryObj.resetToolbar('toolbar', ['bold', 'table']);`, + markdown: `# Cherry API +## resetToolbar(type:string, toolbar:array) +重置工具栏 +type 修改工具栏的类型 {'toolbar'|'toolbarRight'|'sidebar'|'bubble'|'float'} +toolbar 工具栏配置 +${getFullCode(`cherryObj.resetToolbar('toolbar', ['bold', 'table']);`)}`, + }, + { + name: "export", + title: `export(type:string)`, + desc: `导出预览区域的内容,type:{'pdf'|'img'}`, + code: `if(confirm('导出pdf')) { + cherryObj.export(); +}else if(confirm('导出长图')) { + cherryObj.export('img'); +}`, + markdown: `# Cherry API +## export(type:string) +导出预览区域的内容,type:{'pdf'|'img'} +${getFullCode(`if(confirm('导出pdf')) { + cherryObj.export(); + }else if(confirm('导出长图')) { + cherryObj.export('img'); + }` + )}`, + }, + { + name: "switchModel", + title: `switchModel(model:string)`, + desc: `切换模式:{'edit&preview'|'editOnly'|'previewOnly'}`, + code: `if(confirm('只读模式')) { + cherryObj.switchModel('previewOnly'); +}else if(confirm('纯编辑模式')) { + cherryObj.switchModel('editOnly'); +}else if(confirm('双栏编辑模式')) { + cherryObj.switchModel('edit&preview'); +}`, + markdown: `# Cherry API +## switchModel(model:string) +切换模式:{'edit&preview'|'editOnly'|'previewOnly'} +${getFullCode(`if(confirm('只读模式')) { + cherryObj.switchModel('previewOnly'); + }else if(confirm('纯编辑模式')) { + cherryObj.switchModel('editOnly'); + }else if(confirm('双栏编辑模式')) { + cherryObj.switchModel('edit&preview'); + }` + )}`, + }, + { + name: "getToc", + title: `getToc()`, + desc: `获取由标题组成的目录`, + code: `alert(cherryObj.getToc()); +console.log(cherryObj.getToc());`, + markdown: `# Cherry API +## getToc() +获取由标题组成的目录 +${getFullCode(`alert(cherryObj.getToc()); + console.log(cherryObj.getToc());`)}`, + }, + { + name: "getCodeMirror", + title: `getCodeMirror()`, + desc: `获取左侧编辑器实例`, + code: `alert(cherryObj.getCodeMirror()); +console.log(cherryObj.getCodeMirror());`, + markdown: `# Cherry API +## getCodeMirror() +获取左侧编辑器实例 +${getFullCode(`alert(cherryObj.getCodeMirror()); + console.log(cherryObj.getCodeMirror());`)}`, + }, + { + name: "getPreviewer", + title: `getPreviewer()`, + desc: `获取右侧预览区对象实例`, + code: `alert(cherryObj.getPreviewer()); +console.log(cherryObj.getPreviewer());`, + markdown: `# Cherry API +## getPreviewer() +获取右侧预览区对象实例 +${getFullCode(`alert(cherryObj.getPreviewer()); + console.log(cherryObj.getPreviewer());`)}`, + }, + + ], + }, + { + name: "Cherry.Previewer API", + api: [{ + name: "Previewer.scrollToId", + title: `Previewer.scrollToId(id:string)`, + desc: `滚动到对应id的元素位置 +id 可以为带#号hash,也可以是id值`, + code: `// 查看可跳转的id +console.log(cherryObj.getToc()); +// 两种方式都可获得previewer对象 +console.log(cherryObj.previewer == cherryObj.getPreviewer()) +//两种写法都可以 +// cherryObj.previewer.scrollToId('#test-scroll'); +cherryObj.previewer.scrollToId('test-scroll');`, + markdown: `# Cherry API +## Previewer.scrollToId(id:string) +滚动到对应id的元素位置 +id 可以为带#号hash,也可以是id值 +// 查看可跳转的id +${getFullCode(`// 查看可跳转的id + console.log(cherryObj.getToc()); + // 两种方式都可获得previewer对象 + console.log(cherryObj.previewer == cherryObj.getPreviewer()) + // 两种写法都可以 + // cherryObj.previewer.scrollToId('#test-scroll'); + cherryObj.previewer.scrollToId('test-scroll');`)} +# Test Scroll`, + }] + }, + { + name: "Cherry.engine API", + api: [ + { + name: "makeHtml", + title: `engine.makeHtml(markdown:string)`, + desc: `将markdown字符串渲染成Html`, + code: `alert(cherryObj.engine.makeHtml('This is \`inline code\`')); +console.log(cherryObj.engine.makeHtml('This is \`inline code\`'));`, + markdown: `# Cherry.engine API +## engine.makeHtml(markdown:string) +将markdown字符串渲染成Html +${getFullCode(`alert(cherryObj.engine.makeHtml('This is \`inline code\`')); + console.log(cherryObj.engine.makeHtml('This is\`inline code\`'));`)}`, + }, + { + name: "makeMarkdown", + title: `engine.makeMarkdown(html:string)`, + desc: `将html字符串渲染成markdown`, + code: `var html = \` < p > This is<code>inline code</code></ >\`; +alert(cherryObj.engine.makeMarkdown(html)); +console.log(cherryObj.engine.makeMarkdown(html));`, + markdown: `# Cherry.engine API +## engine.makeMarkdown(html:string) +将html字符串渲染成markdown +${getFullCode(`var html = \` < p > This is < code > inline code</ ></ >\`; + alert(cherryObj.engine.makeMarkdown(html)); + console.log(cherryObj.engine.makeMarkdown(html));`)}`, + }, + ], + }, + { + name: "Cherry.toolbar.toolbarHandlers API", + api: [ + { + name: "bold", + title: `toolbar.toolbarHandlers.bold()`, + desc: `在cherry编辑区域的选定文本处插入加粗语法`, + code: `cherryObj.toolbar.toolbarHandlers.bold()`, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.bold() +在cherry编辑区域的选定文本处插入加粗语法 +${getFullCode(`cherryObj.toolbar.toolbarHandlers.bold()`)}`, + }, + { + name: "italic", + title: `toolbar.toolbarHandlers.italic()`, + desc: `在cherry编辑区域的选定文本处插入斜体语法`, + code: `cherryObj.toolbar.toolbarHandlers.italic()`, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.italic() +在cherry编辑区域的选定文本处插入斜体语法 +${getFullCode(`cherryObj.toolbar.toolbarHandlers.italic()`)}`, + }, + { + name: "header", + title: `toolbar.toolbarHandlers.header(level: int)`, + desc: `在cherry编辑区域的选定文本处插入标题语法`, + code: `cherryObj.toolbar.toolbarHandlers.header(1) +// cherryObj.toolbar.toolbarHandlers.header(2) +// cherryObj.toolbar.toolbarHandlers.header(4)`, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.header(level: int) + +在cherry编辑区域的选定文本处插入标题语法 + +${getFullCode(`cherryObj.toolbar.toolbarHandlers.header(1) + // cherryObj.toolbar.toolbarHandlers.header(2) + // cherryObj.toolbar.toolbarHandlers.header(4)`)}`, + }, + { + name: "strikethrough", + title: `toolbar.toolbarHandlers.strikethrough()`, + desc: `在cherry编辑区域的选定文本处插入删除线语法`, + code: `cherryObj.toolbar.toolbarHandlers.strikethrough()`, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.strikethrough() +在cherry编辑区域的选定文本处插入删除线语法 +${getFullCode(`cherryObj.toolbar.toolbarHandlers.strikethrough()`)}`, + }, + { + name: "list", + title: `toolbar.toolbarHandlers.list(type: string)`, + desc: `在cherry编辑区域的选定文本处插入有序、无序列表或者checklist语法`, + code: `if(confirm('有序列表')) { + cherryObj.toolbar.toolbarHandlers.list(1); + }else if (confirm('无序列表')) { + cherryObj.toolbar.toolbarHandlers.list('2'); + } else if (confirm('checklist')) { + cherryObj.toolbar.toolbarHandlers.list(3); + } `, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.list(type: string) +在cherry编辑区域的选定文本处插入有序、无序列表或者checklist语法 +| level | 效果 | +|:-:|:-:| +| 1 | ol 列表 | +| 2 | ul 列表 | +| 3 | checklist | +${getFullCode(`if(confirm('有序列表')) { + cherryObj.toolbar.toolbarHandlers.list(1); + }else if (confirm('无序列表')) { + cherryObj.toolbar.toolbarHandlers.list('2'); + } else if (confirm('checklist')) { + cherryObj.toolbar.toolbarHandlers.list(3); + } `)}`, + }, + { + name: "insert", + title: `toolbar.toolbarHandlers.insert(type: string)`, + desc: `在cherry编辑区域的光标处插入特定语法`, + code: `if (confirm('插入3*4的表格')) { + cherryObj.toolbar.toolbarHandlers.insert('normal-table-3*4'); +} else if (confirm('插入checklist')) { + cherryObj.toolbar.toolbarHandlers.insert('checklist'); +} `, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.insert(type: string) +在cherry编辑区域的光标处插入特定语法: + +| type | 效果 | +|:-:|:-:| +| 'hr' | 删除线 | +| 'br' | 强制换行 | +| 'code' | 代码块 | +| 'formula' | 行内公式 | +| 'checklist' | 检查项 | +| 'toc' | 目录 | +| 'link' | 超链接 | +| 'image' | 图片 | +| 'video' | 视频 | +| 'audio' | 音频 | +| 'normal-table' | 插入3行5列的表格 | +| 'normal-table-row*col' | 如 \`normal-table-2*4\` 插入2行(包含表头是3行)4列的表格 | + +${getFullCode(`if (confirm('插入3*4的表格')) { + cherryObj.toolbar.toolbarHandlers.insert('normal-table-3*4'); + } else if (confirm('插入checklist')) { + cherryObj.toolbar.toolbarHandlers.insert('checklist'); + } `)}`, + }, + { + name: "graph", + title: `toolbar.toolbarHandlers.graph(type:string)`, + desc: `在cherry编辑区域的光标处插入画图语法`, + code: `cherryObj.toolbar.toolbarHandlers.graph(1) +// cherryObj.toolbar.toolbarHandlers.graph('2') +// cherryObj.toolbar.toolbarHandlers.graph(4)`, + markdown: `# Cherry.toolbar.toolbarHandlers API +## toolbar.toolbarHandlers.graph(type:string) +在cherry编辑区域的光标处插入画图语法 + +|id |效果 | +|:-:|:-:| +|'1' |流程图 | +|'2' |时序图 | +|'3' |状态图 | +|'4' |类图 | +|'5' |饼图 | +|'6' |甘特图 | +\`\`\`mermaid +graph LR + A[公司] -->| 下 班 | B(菜市场) + B --> C{看见<br>卖西瓜的} + C -->|Yes| D[买一个包子] + C -->|No| E[买一斤包子] +\`\`\` +${getFullCode(`cherryObj.toolbar.toolbarHandlers.graph(1) + // cherryObj.toolbar.toolbarHandlers.graph('2') + // cherryObj.toolbar.toolbarHandlers.graph(4)`)}`, + }, + ] + } +] + +const CodeTextArea = ({ value, onChange }) => { + const ref = useRef(null); + useEffect(() => { + // 高度适应内容 + const resize = () => { + if (ref.current) { + ref.current.style.height = 'auto'; // 重置高度 + ref.current.style.height = `${ref.current.scrollHeight}px`; // 设置新高度 + } + }; + resize(); + }, [value]); + return ( + <textarea ref={ref} className="code-input" placeholder="请输入可执行的js代码,编辑器对象为'cherryObj'" onChange={e => onChange(e.target.value)} value={value} /> + ) +} + +export default ({ cherryObj }) => { + const [selectedGroup, setSelectedGroup] = useState(0); + const [selectedApi, setSelectedApi] = useState(0); + const [title, setTitle] = useState(""); + const [desc, setDesc] = useState(""); + const [code, setCode] = useState(""); + const [allowReset, setAllowReset] = useState(false); + const handleOptionChange = (groupIdx, apiIdx) => { + setSelectedGroup(groupIdx); + setSelectedApi(apiIdx); + const content = data[groupIdx].api[apiIdx]; + setCode(content.code); + setDesc(content.desc); + setTitle(content.title); + cherryObj.setMarkdown(content.markdown); + } + const reset = () => { + const content = data[selectedGroup].api[selectedApi]; + setCode(content.code); + cherryObj.setMarkdown(content.markdown); + } + const computeAllowReset = () => { + const content = data[selectedGroup].api[selectedApi]; + const allow = content.name != "resetToolbar" && content.name != "destroy"; + setAllowReset(allow); + return + } + useEffect(() => { + if (cherryObj) { + handleOptionChange(selectedGroup, selectedApi); + } + computeAllowReset() + }, [cherryObj, selectedGroup, selectedApi]) + return ( + <div className="menu"> + <div> + <span>选择例子</span> + {cherryObj && <> + <Select options={data.map(datum => datum.name)} onChange={(idx) => { setSelectedGroup(idx); setSelectedApi(0) }} selected={selectedGroup} /> + <Select options={data[selectedGroup].api.map(datum => datum.name)} onChange={setSelectedApi} selected={selectedApi} /> + </>} + </div> + <h2>{title}</h2> + <h3>描述</h3> + <div className="demo-desc">{desc}</div> + <h3> + <span>代码示例</span> + {cherryObj && <> + <button className="run-btn" onClick={() => { eval(code); }}>点击执行</button> + <button className="run-btn" onClick={reset} disabled={!allowReset}>{allowReset ? "重置" : "请通过刷新恢复"}</button> + </>} + </h3> + <CodeTextArea value={code} onChange={setCode} /> + </div> + ) +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/components/Title.jsx b/examples/cherry-markdown-react-demo/src/components/Title.jsx new file mode 100644 index 00000000..09240f00 --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/components/Title.jsx @@ -0,0 +1,7 @@ +export default () => { + return ( + <div className="title-wrapper"> + <h2 className='title'>React Demo</h2> + </div> + ) +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/index.css b/examples/cherry-markdown-react-demo/src/index.css new file mode 100644 index 00000000..3e3b6a19 --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} \ No newline at end of file diff --git a/examples/cherry-markdown-react-demo/src/index.js b/examples/cherry-markdown-react-demo/src/index.js new file mode 100644 index 00000000..f45e211e --- /dev/null +++ b/examples/cherry-markdown-react-demo/src/index.js @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + <App /> +); \ No newline at end of file diff --git a/examples/scripts/api-demo.js b/examples/scripts/api-demo.js index f99fb492..64e16e50 100644 --- a/examples/scripts/api-demo.js +++ b/examples/scripts/api-demo.js @@ -77,6 +77,7 @@ var cherryConfig = { 'settings', 'export' ], + hiddenToolbar: ['panel', 'justify'], }, editor: {}, previewer: { diff --git a/examples/scripts/index-demo.js b/examples/scripts/index-demo.js index a25ce9a7..8c0580d5 100644 --- a/examples/scripts/index-demo.js +++ b/examples/scripts/index-demo.js @@ -179,7 +179,7 @@ var basicConfig = { '|', 'formula', { - insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'pdf', 'word'], + insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'inlineCode', 'formula', 'toc', 'table', 'pdf', 'word'], }, 'graph', 'customMenuTable', @@ -193,7 +193,7 @@ var basicConfig = { 'customMenuCName', 'theme', ], - toolbarRight: ['fullScreen', '|'], + toolbarRight: ['fullScreen', '|', 'wordCount'], bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false sidebar: ['mobilePreview', 'copy', 'theme', 'publish'], sidebar: ['mobilePreview', 'copy', 'theme'], @@ -216,7 +216,7 @@ var basicConfig = { // serviceUrl: 'http://localhost:3001', // injectPayload: { // thumb_media_id: 'ft7IwCi1eukC6lRHzmkYuzeMmVXWbU3JoipysW2EZamblyucA67wdgbYTix4X377', - // author: 'Cherry Markdown', + // author: 'Cherry Markdown', // }, // } // ], diff --git a/src/Cherry.config.js b/src/Cherry.config.js index 15b4ca0d..2350f5f7 100644 --- a/src/Cherry.config.js +++ b/src/Cherry.config.js @@ -312,6 +312,7 @@ const defaultConfig = { sidebar: false, bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', '|', 'size', 'color'], // array or false float: ['h1', 'h2', 'h3', '|', 'checklist', 'quote', 'table', 'code'], // array or false + hiddenToolbar: [], // 不展示在编辑器中的工具栏,只使用工具栏的api和快捷键功能 toc: false, // 不展示悬浮目录 // toc: { // updateLocationHash: false, // 要不要更新URL的hash diff --git a/src/Cherry.js b/src/Cherry.js index 2e6f848d..4a97eae1 100644 --- a/src/Cherry.js +++ b/src/Cherry.js @@ -24,6 +24,7 @@ import ToolbarRight from './toolbars/ToolbarRight'; import Toc from './toolbars/Toc'; import { createElement } from './utils/dom'; import Sidebar from './toolbars/Sidebar'; +import HiddenToolbar from './toolbars/HiddenToolbar'; import { customizer, getThemeFromLocal, changeTheme, getCodeThemeFromLocal } from './utils/config'; import NestedError, { $expectTarget } from './utils/error'; import getPosBydiffs from './utils/recount-pos'; @@ -188,6 +189,7 @@ export default class Cherry extends CherryStatic { this.wrapperDom = wrapperDom; // 创建预览区域的侧边工具栏 this.createSidebar(); + this.createHiddenToolbar(); mountEl.appendChild(wrapperDom); editor.init(previewer); @@ -592,6 +594,7 @@ export default class Cherry extends CherryStatic { this.createBubble(); this.createFloatMenu(); this.createSidebar(); + this.createHiddenToolbar(); return true; } @@ -637,6 +640,19 @@ export default class Cherry extends CherryStatic { } } + createHiddenToolbar() { + console.log(this.options.toolbars.hiddenToolbar); + if (this.options.toolbars.hiddenToolbar) { + $expectTarget(this.options.toolbars.hiddenToolbar, Array); + this.hiddenToolbar = new HiddenToolbar({ + $cherry: this, + buttonConfig: this.options.toolbars.hiddenToolbar, + customMenu: this.options.toolbars.customMenu, + }); + this.toolbar.collectMenuInfo(this.hiddenToolbar); + } + } + /** * @private * @returns diff --git a/src/core/hooks/CodeBlock.js b/src/core/hooks/CodeBlock.js index 4e4341c3..de24c7c4 100644 --- a/src/core/hooks/CodeBlock.js +++ b/src/core/hooks/CodeBlock.js @@ -189,7 +189,7 @@ export default class CodeBlock extends ParagraphBase { */ renderCodeBlock($code, $lang, sign, lines) { let cacheCode = $code; - let lang = $lang; + let lang = $lang.toLowerCase(); if (this.customHighlighter) { // 平台自定义代码块样式 cacheCode = this.customHighlighter(cacheCode, lang); diff --git a/src/core/hooks/SuggestList.js b/src/core/hooks/SuggestList.js index 5019f5df..dc3d358a 100644 --- a/src/core/hooks/SuggestList.js +++ b/src/core/hooks/SuggestList.js @@ -261,14 +261,14 @@ const MoreSuggestList = [ }, { icon: 'FullWidth', - label: '行内代码', + label: 'inlineCode', keyword: '`', value: '``', goLeft: 1, }, { icon: 'FullWidth', - label: '代码块', + label: 'codeBlock', keyword: '`', value: '```\n\n```\n', goTop: 2, @@ -316,10 +316,10 @@ export function allSuggestList(keyword, locales) { const systemSuggestList = [].concat(SystemSuggestList); const otherSuggestList = [].concat(OtherSuggestList); systemSuggestList.forEach((item) => { - item.label = locales ? locales[item.label] : item.label; + item.label = locales[item.label] || item.label; }); otherSuggestList.forEach((item) => { - item.label = locales ? locales[item.label] : item.label; + item.label = locales[item.label] || item.label; }); if (keyword[0] === '/' || keyword[0] === '、' || addonsKeywords.includes(keyword[0])) { systemSuggestList.forEach((item) => { diff --git a/src/core/hooks/Suggester.js b/src/core/hooks/Suggester.js index 709365a6..2664b222 100644 --- a/src/core/hooks/Suggester.js +++ b/src/core/hooks/Suggester.js @@ -105,6 +105,7 @@ export default class Suggester extends SyntaxBase { this.suggester = {}; const defaultSuggest = []; + const that = this; // 默认的唤醒关键字 for (const suggesterKeyword of suggesterKeywords) { defaultSuggest.push({ @@ -112,7 +113,7 @@ export default class Suggester extends SyntaxBase { suggestList(_word, callback) { // 将word全转成小写 const word = _word.toLowerCase(); - const systemSuggestList = allSuggestList(suggesterKeyword, this.$locale); + const systemSuggestList = allSuggestList(suggesterKeyword, that.$locale); // 加个空格就直接退出联想 if (/^\s$/.test(word)) { callback(false); diff --git a/src/locales/en_US.js b/src/locales/en_US.js index 0a496477..90c01d4d 100644 --- a/src/locales/en_US.js +++ b/src/locales/en_US.js @@ -90,7 +90,6 @@ export default { justifyCenter: 'Center', justifyRight: 'Right', publish: 'Publish', - wordCount: 'Word Count', fontColor: 'Font Color', fontBgColor: 'Font Bg Color', small: 'Small', @@ -99,5 +98,11 @@ export default { superLarge: 'Super Large', detailDefaultContent: 'Click to expand more\nContent\n++- Expand by default\nContent\n++ Collapse by default\nContent', + inlineCode: 'Inline Code', + codeBlock: 'Code Block', editShortcutKeyConfigTip: 'double click shortcut key area to edit', + wordCount: 'Word Count', + wordCountP: 'P', + wordCountW: 'W', + wordCountC: 'C', }; diff --git a/src/locales/zh_CN.js b/src/locales/zh_CN.js index 07979c61..e52e0653 100644 --- a/src/locales/zh_CN.js +++ b/src/locales/zh_CN.js @@ -94,7 +94,6 @@ export default { justifyCenter: '居中', justifyRight: '右对齐', publish: '发布', - wordCount: '字数', fontColor: '文本颜色', fontBgColor: '背景颜色', small: '小', @@ -102,5 +101,11 @@ export default { large: '大', superLarge: '特大', detailDefaultContent: '点击展开更多\n内容\n++- 默认展开\n内容\n++ 默认收起\n内容', + inlineCode: '行内代码', + codeBlock: '代码块', editShortcutKeyConfigTip: '双击快捷键区域编辑快捷键', + wordCount: '字数统计', + wordCountP: '段落', + wordCountW: '单词', + wordCountC: '字符', }; diff --git a/src/sass/ch-icon.scss b/src/sass/ch-icon.scss index cd5418ce..59b37e24 100644 --- a/src/sass/ch-icon.scss +++ b/src/sass/ch-icon.scss @@ -108,4 +108,6 @@ .ch-icon-justifyRight:before { content: "\EA6F" } .ch-icon-chevronsLeft:before { content: "\EA70" } .ch-icon-chevronsRight:before { content: "\EA71" } -.ch-icon-trendingUp:before { content: "\EA72" } \ No newline at end of file +.ch-icon-trendingUp:before { content: "\EA72" } +.ch-icon-inlineCode:before { content: "\EA73" } +.ch-icon-codeBlock:before { content: "\EA74" } \ No newline at end of file diff --git a/src/sass/components/shortcut_key_config.scss b/src/sass/components/shortcut_key_config.scss index 7b3f3b49..f84e8b22 100644 --- a/src/sass/components/shortcut_key_config.scss +++ b/src/sass/components/shortcut_key_config.scss @@ -32,6 +32,9 @@ .shortcut-key-config-panel-kbd { display: flex; gap: 10px; + min-width: 120px; + justify-content: right; + .keyboard-key { border-radius: 3px; border-style: solid; @@ -43,7 +46,7 @@ vertical-align: middle; line-height: 20px; margin: 4px; - width: 10px; + min-width: 16px; text-align: center; } } diff --git a/src/sass/icons/uEA73-inlineCode.svg b/src/sass/icons/uEA73-inlineCode.svg new file mode 100644 index 00000000..9c124fc6 --- /dev/null +++ b/src/sass/icons/uEA73-inlineCode.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="图形" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="90.288 58.809 850.394 850.394" enable-background="new 90.288 58.809 850.394 850.394" xml:space="preserve"> +<g> + <path d="M350.505,641.598L166.106,484.626l184.79-162.875c12.429-10.956,13.625-29.913,2.669-42.343 + c-10.955-12.429-29.912-13.624-42.343-2.669L100.451,462.515c-6.529,5.755-10.237,14.062-10.162,22.764 + c0.075,8.703,3.926,16.944,10.553,22.585l210.771,179.421c5.646,4.807,12.556,7.157,19.432,7.157 + c8.489-0.001,16.924-3.584,22.858-10.555C364.643,671.272,363.122,652.337,350.505,641.598z"/> + <path d="M930.519,464.678L719.747,278.903c-12.431-10.955-31.387-9.759-42.343,2.669c-10.955,12.43-9.761,31.387,2.669,42.343 + L864.864,486.79l-184.4,156.971c-12.616,10.739-14.138,29.674-3.397,42.29c5.934,6.971,14.368,10.555,22.858,10.555 + c6.875-0.001,13.786-2.352,19.432-7.157l210.772-179.42c6.627-5.642,10.478-13.883,10.553-22.586 + C940.756,478.74,937.047,470.433,930.519,464.678z"/> + <path d="M582.347,201.347c-16.13-3.813-32.287,6.165-36.101,22.289L426.335,730.564c-3.814,16.124,6.165,32.286,22.289,36.101 + c2.323,0.55,4.644,0.813,6.931,0.813c13.591,0,25.904-9.3,29.169-23.101l119.912-506.929 + C608.45,221.323,598.47,205.161,582.347,201.347z"/> +</g> +</svg> diff --git a/src/sass/icons/uEA74-codeBlock.svg b/src/sass/icons/uEA74-codeBlock.svg new file mode 100644 index 00000000..b2fe6397 --- /dev/null +++ b/src/sass/icons/uEA74-codeBlock.svg @@ -0,0 +1 @@ +<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M832 992H192C102.4 992 32 921.6 32 832V192C32 102.4 102.4 32 192 32h640c89.6 0 160 70.4 160 160v640c0 89.6-70.4 160-160 160zM192 96c-51.2 0-96 44.8-96 96v640c0 51.2 44.8 96 96 96h640c51.2 0 96-44.8 96-96V192c0-51.2-44.8-96-96-96H192z" fill="#000000" p-id="2464"></path><path d="M339.2 704c-6.4 0-19.2-6.4-25.6-12.8L192 556.8c-19.2-25.6-19.2-64 0-89.6l121.6-140.8c12.8-6.4 32-12.8 44.8 0 12.8 12.8 12.8 32 6.4 44.8L243.2 512l121.6 140.8c12.8 12.8 12.8 32-6.4 44.8-6.4 6.4-12.8 6.4-19.2 6.4zM691.2 704c-6.4 0-12.8 0-19.2-6.4-12.8-12.8-12.8-32-6.4-44.8L780.8 512l-121.6-140.8c-12.8-12.8-12.8-32 6.4-44.8 12.8-12.8 32-12.8 44.8 6.4L832 467.2c19.2 25.6 19.2 57.6 0 83.2l-121.6 140.8c-6.4 6.4-12.8 12.8-19.2 12.8z" fill="#000000" p-id="2465"></path><path d="M448 704h-12.8c-19.2-6.4-25.6-25.6-19.2-38.4l128-320c6.4-19.2 25.6-32 44.8-25.6 19.2 6.4 25.6 25.6 19.2 38.4l-128 320c-6.4 19.2-19.2 25.6-32 25.6z" fill="#000000"></path></svg> diff --git a/src/toolbars/HiddenToolbar.js b/src/toolbars/HiddenToolbar.js new file mode 100644 index 00000000..f81d468b --- /dev/null +++ b/src/toolbars/HiddenToolbar.js @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2021 THL A29 Limited, a Tencent company. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Toolbar from './Toolbar'; +/** + * 预览区域右侧悬浮的工具栏 + * 推荐放置跟编辑区域完全无关的工具栏 + * 比如复制预览区域内容、修改预览区域主题等 + */ +export default class HiddenToolbar extends Toolbar { + // constructor(options) { + // super(options); + // } + appendMenusToDom(menus) { + return; + } +} diff --git a/src/toolbars/HookCenter.js b/src/toolbars/HookCenter.js index c0b987b9..c7d2ce96 100644 --- a/src/toolbars/HookCenter.js +++ b/src/toolbars/HookCenter.js @@ -41,6 +41,7 @@ import FullScreen from './hooks/FullScreen'; import Undo from './hooks/Undo'; import Redo from './hooks/Redo'; import Code from './hooks/Code'; +import InlineCode from './hooks/InlineCode'; import CodeTheme from './hooks/CodeTheme'; import Export from './hooks/Export'; import Settings from './hooks/Settings'; @@ -99,6 +100,7 @@ const HookList = { quickTable: QuickTable, togglePreview: TogglePreview, code: Code, + inlineCode: InlineCode, codeTheme: CodeTheme, export: Export, settings: Settings, @@ -175,13 +177,14 @@ export default class HookCenter { */ const currentMenuOptions = options || { name, icon: name }; const { $cherry, customMenu } = this.toolbar.options; + $cherry.$currentMenuOptions = currentMenuOptions; if (HookList[name]) { this.allMenusName.push(name); - this.hooks[name] = new HookList[name]({ ...$cherry, $currentMenuOptions: currentMenuOptions }); + this.hooks[name] = new HookList[name]($cherry); } else if (customMenu !== undefined && customMenu !== null && customMenu[name]) { this.allMenusName.push(name); // 如果是自定义菜单,传参兼容旧版 - this.hooks[name] = new customMenu[name]({ ...$cherry, $currentMenuOptions: currentMenuOptions }); + this.hooks[name] = new customMenu[name]($cherry); } } diff --git a/src/toolbars/hooks/Code.js b/src/toolbars/hooks/Code.js index 476ba151..1aaae720 100644 --- a/src/toolbars/hooks/Code.js +++ b/src/toolbars/hooks/Code.js @@ -24,7 +24,7 @@ export default class Code extends MenuBase { */ constructor($cherry) { super($cherry); - this.setName('code', 'code'); + this.setName('codeBlock', 'codeBlock'); this.shortcutKeyMap = { [`${CONTROL_KEY}-${getKeyCode('k')}`]: { hookName: this.name, diff --git a/src/toolbars/hooks/Color.js b/src/toolbars/hooks/Color.js index 27f79825..590699fd 100644 --- a/src/toolbars/hooks/Color.js +++ b/src/toolbars/hooks/Color.js @@ -35,18 +35,31 @@ export default class Color extends MenuBase { return bgReg.test(selection); } + $testIsShortKey(shortKey) { + return /(color|background-color)\s*:/.test(shortKey); + } + + $getTypeAndColor(shortKey) { + if (this.$testIsShortKey(shortKey)) { + const type = /background-color\s*:/.test(shortKey) ? 'background-color' : 'text'; + const color = shortKey.replace(/(color|background-color)\s*:\s*([#0-9a-zA-Z]+)[^#0-9a-zA-Z]*$/, '$2').trim(); + return { type, color }; + } + return this.getAndCleanCacheOnce(); + } + /** * 响应点击事件 * @param {string} selection 被用户选中的文本内容 - * @param {string} shortKey 快捷键参数,本函数不处理这个参数 + * @param {string} shortKey 快捷键参数,color: #000000 | background-color: #000000 * @param {Event & {target:HTMLElement}} event 点击事件,用来从被点击的调色盘中获得对应的颜色 * @returns 回填到编辑器光标位置/选中文本区域的内容 */ onClick(selection, shortKey = '', event) { let $selection = getSelection(this.editor.editor, selection) || this.locale.color; - if (this.hasCacheOnce()) { + if (this.hasCacheOnce() || this.$testIsShortKey(shortKey)) { // @ts-ignore - const { type, color } = this.getAndCleanCacheOnce(); + const { type, color } = this.$getTypeAndColor(shortKey); const begin = type === 'text' ? `!!${color} ` : `!!!${color} `; const end = type === 'text' ? '!!' : '!!!'; if (!this.isSelections && !this.$testIsColor(type, $selection)) { diff --git a/src/toolbars/hooks/InlineCode.js b/src/toolbars/hooks/InlineCode.js new file mode 100644 index 00000000..4127b344 --- /dev/null +++ b/src/toolbars/hooks/InlineCode.js @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2021 THL A29 Limited, a Tencent company. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import MenuBase from '@/toolbars/MenuBase'; +import { CONTROL_KEY } from '@/utils/shortcutKey'; +/** + * 插入行内代码的按钮 + */ +export default class InlineCode extends MenuBase { + constructor($cherry) { + super($cherry); + this.setName('inlineCode', 'inlineCode'); + this.shortcutKeyMap = { + [`${CONTROL_KEY}-Backquote`]: { + hookName: this.name, + aliasName: this.$cherry.locale[this.name], + }, + }; + } + + /** + * 响应点击事件 + * @param {string} selection 被用户选中的文本内容 + * @param {string} shortKey 快捷键参数,本函数不处理这个参数 + * @returns {string} 回填到编辑器光标位置/选中文本区域的内容 + */ + onClick(selection, shortKey = '') { + if (!selection) { + this.registerAfterClickCb(() => this.setLessSelection('`', '`')); + return '``'; + } + + if (!selection.includes('\n')) { + this.registerAfterClickCb(() => this.setLessSelection('`', '`')); + return `\`${selection}\``; + } + + const arr = selection.split('\n').map((item) => `\`${item}\``); + return arr.join('\n'); + } +} diff --git a/src/toolbars/hooks/WordCount.js b/src/toolbars/hooks/WordCount.js index f83da239..767fdc62 100644 --- a/src/toolbars/hooks/WordCount.js +++ b/src/toolbars/hooks/WordCount.js @@ -32,18 +32,29 @@ export default class wordCount extends MenuBase { * @returns {string} 回填到编辑器光标位置/选中文本区域的内容 */ onClick(selection, shortKey = '') { - const span = document.querySelector('.cherry-toolbar-button.cherry-toolbar-wordCount'); + const span = this.$cherry.wrapperDom.querySelector('.cherry-toolbar-button.cherry-toolbar-wordCount'); // 首次点击时添加监听器 if (this.countState === 0) { span.addEventListener('count', () => { const markdown = this.$cherry.getMarkdown(); const { characters, words, paragraphs } = this.wordCount(markdown); - if (this.countState === 1) { - span.innerHTML = `P ${paragraphs}`; - } else if (this.countState === 2) { - span.innerHTML = `W ${words}`; - } else { - span.innerHTML = `C ${characters}`; + const { locale } = this.$cherry; + switch (this.countState) { + case 0: + span.innerHTML = locale.wordCount; + break; + case 1: + span.innerHTML = `${locale.wordCountC} ${characters}`; + break; + case 2: + span.innerHTML = `${locale.wordCountW} ${words}`; + break; + case 3: + span.innerHTML = `${locale.wordCountP} ${paragraphs}`; + break; + case 4: + span.innerHTML = `${locale.wordCountC} ${characters}   ${locale.wordCountW} ${words}   ${locale.wordCountP} ${paragraphs}`; + break; } }); @@ -60,8 +71,11 @@ export default class wordCount extends MenuBase { }, 500); }); } - // 循环切换3种状态 - this.countState = ((this.countState + 1) % 3) + 1; + // 循环切换4种状态 + this.countState += 1; + if (this.countState > 4) { + this.countState = 0; + } span.dispatchEvent(this.countEvent); return selection; } diff --git a/src/utils/code-preview-language-setting.js b/src/utils/code-preview-language-setting.js index 0d2423bf..83a62458 100644 --- a/src/utils/code-preview-language-setting.js +++ b/src/utils/code-preview-language-setting.js @@ -18,8 +18,7 @@ export const getCodePreviewLangSelectElement = (lang) => { </select>`; }; -// program language list: -export const codePreviewLangSelectList = [ +const allCodeLangPrismSupport = [ 'javascript', 'typescript', 'html', @@ -46,4 +45,139 @@ export const codePreviewLangSelectList = [ 'sql', 'xml', 'svg', + // TODO: 后续可以取Prism.languages,而不是人工维护了 + 'adoc', + 'asciidoc', + 'asm6502', + 'aspnet', + 'atom', + 'awk', + 'bash', + 'basic', + 'batch', + 'c', + 'clike', + 'cmake', + 'context', + 'cpp', + 'cs', + 'csharp', + 'css', + 'csv', + 'dart', + 'diff', + 'django', + 'dns-zone', + 'dns-zone-file', + 'docker', + 'dockerfile', + 'dotnet', + 'extend', + 'ftl', + 'gawk', + 'git', + 'glsl', + 'go', + 'go-mod', + 'go-module', + 'graphql', + 'haml', + 'html', + 'http', + 'ini', + 'insertBefore', + 'java', + 'javadoc', + 'javadoclike', + 'javascript', + 'javastacktrace', + 'jinja2', + 'js', + 'jsdoc', + 'json', + 'json5', + 'jsonp', + 'jsstacktrace', + 'jsx', + 'latex', + 'ld', + 'less', + 'linker-script', + 'lua', + 'makefile', + 'markdown', + 'markup', + 'markup-templating', + 'mathml', + 'matlab', + 'md', + 'mermaid', + 'mongodb', + 'nasm', + 'nginx', + 'nsis', + 'objc', + 'objectivec', + 'objectpascal', + 'pascal', + 'perl', + 'php', + 'phpdoc', + 'plain', + 'plaintext', + 'plant-uml', + 'plantuml', + 'plsql', + 'powershell', + 'properties', + 'protobuf', + 'py', + 'python', + 'r', + 'rb', + 'regex', + 'rss', + 'ruby', + 'rust', + 'sas', + 'sass', + 'scala', + 'scheme', + 'scss', + 'sh-session', + 'shell', + 'shell-session', + 'shellsession', + 'smali', + 'splunk-spl', + 'sql', + 'ssml', + 'svg', + 'swift', + 'systemd', + 'tex', + 'text', + 'textile', + 'ts', + 'tsx', + 'txt', + 'typescript', + 'uri', + 'url', + 'vb', + 'vba', + 'vbnet', + 'vim', + 'visual-basic', + 'wasm', + 'webmanifest', + 'wiki', + 'xml', + 'yaml', + 'yml', ]; + +// program language list: +export const codePreviewLangSelectList = allCodeLangPrismSupport.filter( + (item, index) => allCodeLangPrismSupport.indexOf(item) === index, +);