├── .env
├── .eslintignore
├── .eslintrc
├── .github
├── actions
│ ├── build-core
│ │ └── action.yml
│ ├── build-svelte
│ │ └── action.yml
│ ├── build-vue
│ │ └── action.yml
│ └── install-dependencies
│ │ └── action.yml
└── workflows
│ ├── ci.yml
│ └── publish.yml
├── .gitignore
├── .npmrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── env.d.ts
├── examples
├── demo.ts
├── hooks.test.ts
├── hooks.ts
├── minimal.test.ts
├── minimal.ts
├── plugins.tsx
├── template-ts
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── main.ts
│ │ └── vite-env.d.ts
│ └── tsconfig.json
└── web-component.ts
├── index.html
├── lib
└── codemirror-kit
│ ├── decorations.ts
│ ├── index.ts
│ ├── markdown.ts
│ └── parsers.ts
├── package.json
├── playwright.config.ts
├── plugins
└── katex
│ ├── grammar.ts
│ └── index.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── screenshot.png
├── src
├── api
│ ├── destroy.ts
│ ├── focus.ts
│ ├── format.ts
│ ├── get_doc.ts
│ ├── index.ts
│ ├── insert.ts
│ ├── load.ts
│ ├── options.ts
│ ├── reconfigure.ts
│ ├── select.ts
│ ├── selections.ts
│ ├── update.ts
│ └── wrap.ts
├── constants.ts
├── editor
│ ├── adapters
│ │ └── selections.ts
│ ├── extensions
│ │ ├── appearance.ts
│ │ ├── autocomplete.ts
│ │ ├── blockquote.ts
│ │ ├── code.ts
│ │ ├── extension.ts
│ │ ├── images.ts
│ │ ├── indentWithTab.ts
│ │ ├── ink.ts
│ │ ├── line_wrapping.ts
│ │ ├── lists.ts
│ │ ├── placeholder.ts
│ │ ├── readonly.ts
│ │ ├── search.tsx
│ │ ├── spellcheck.ts
│ │ ├── theme.ts
│ │ └── vim.ts
│ ├── index.ts
│ ├── state.ts
│ └── view.ts
├── extensions.ts
├── index.tsx
├── instance.ts
├── markdown.ts
├── store.ts
├── ui
│ ├── app.tsx
│ ├── components
│ │ ├── button
│ │ │ └── index.tsx
│ │ ├── details
│ │ │ └── index.tsx
│ │ ├── drop_zone
│ │ │ ├── index.tsx
│ │ │ └── styles.css
│ │ ├── editor
│ │ │ └── index.tsx
│ │ ├── root
│ │ │ ├── index.tsx
│ │ │ ├── styles.css
│ │ │ └── styles.tsx
│ │ └── toolbar
│ │ │ ├── index.tsx
│ │ │ └── styles.css
│ └── utils.ts
└── utils
│ ├── awaitable.ts
│ ├── inspect.ts
│ ├── merge.ts
│ ├── options.ts
│ ├── queue.ts
│ └── readability.ts
├── svelte
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── package.json
├── src
│ ├── app.d.ts
│ ├── app.html
│ ├── lib
│ │ ├── InkMde.svelte
│ │ └── index.js
│ └── routes
│ │ └── +page.svelte
├── static
│ └── favicon.png
├── svelte.config.js
├── tsconfig.json
└── vite.config.js
├── test
├── assets
│ └── example.md
├── e2e
│ ├── index.html
│ └── plugins
│ │ └── autocomplete.spec.ts
├── helpers
│ └── dom.ts
├── mocks
│ ├── editor.ts
│ └── store.ts
└── unit
│ └── src
│ ├── api
│ └── index.test.ts
│ └── index.test.ts
├── tsconfig.json
├── types.config.js
├── types
├── index.ts
├── ink.ts
├── internal.ts
├── ui.ts
└── values.ts
├── vite.config.ts
└── vue
├── dev
├── app.ts
├── client.ts
└── server.ts
├── env.d.ts
├── index.html
├── server.ts
├── src
├── InkMde.vue
└── index.ts
├── tsconfig.json
├── types.config.js
└── vite.config.ts
/.env:
--------------------------------------------------------------------------------
1 | VITE_SSR=
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | svelte
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["artisan", "plugin:solid/typescript"],
3 | "plugins": ["solid"],
4 | "rules": {
5 | "solid/components-return-once": ["off", {}]
6 | },
7 | "overrides": [
8 | {
9 | "files": ["./**/*.md/*.ts"],
10 | "rules": {
11 | "@typescript-eslint/no-unused-vars": ["off"]
12 | }
13 | },
14 | {
15 | "files": ["./src/index.tsx"],
16 | "rules": {
17 | "eslint-comments/no-unlimited-disable": ["off", {}]
18 | }
19 | },
20 | {
21 | "files": ["./types/**/*"],
22 | "rules": {
23 | "@typescript-eslint/no-namespace": ["off"]
24 | }
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/.github/actions/build-core/action.yml:
--------------------------------------------------------------------------------
1 | name: Build core
2 | description: Build the core package
3 | runs:
4 | using: composite
5 | steps:
6 | - shell: bash
7 | run: pnpm core:build
8 |
--------------------------------------------------------------------------------
/.github/actions/build-svelte/action.yml:
--------------------------------------------------------------------------------
1 | name: Build Svelte
2 | description: Build the Svelte wrapper
3 | runs:
4 | using: composite
5 | steps:
6 | - uses: ./.github/actions/build-core
7 | - shell: bash
8 | run: pnpm svelte:build
9 |
--------------------------------------------------------------------------------
/.github/actions/build-vue/action.yml:
--------------------------------------------------------------------------------
1 | name: Build Vue
2 | description: Build the Vue wrapper
3 | runs:
4 | using: composite
5 | steps:
6 | - uses: ./.github/actions/build-core
7 | - shell: bash
8 | run: pnpm vue:build
9 |
--------------------------------------------------------------------------------
/.github/actions/install-dependencies/action.yml:
--------------------------------------------------------------------------------
1 | name: Install dependencies
2 | description: Install with cached dependencies
3 | runs:
4 | using: composite
5 | steps:
6 | - uses: actions/setup-node@v4
7 | with:
8 | node-version: 18
9 | - uses: pnpm/action-setup@v4
10 | with:
11 | run_install: false
12 | - shell: bash
13 | run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
14 | - uses: actions/cache@v3
15 | with:
16 | key: v1-${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
17 | path: ${{ env.STORE_PATH }}
18 | restore-keys: v1-${{ runner.os }}-pnpm-store-
19 | - shell: bash
20 | run: pnpm install
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | build-core:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: ./.github/actions/install-dependencies
13 | - uses: ./.github/actions/build-core
14 | build-svelte:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: ./.github/actions/install-dependencies
19 | - uses: ./.github/actions/build-core
20 | - uses: ./.github/actions/build-svelte
21 | build-vue:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v3
25 | - uses: ./.github/actions/install-dependencies
26 | - uses: ./.github/actions/build-core
27 | - uses: ./.github/actions/build-vue
28 | check-docs:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - uses: actions/checkout@v3
32 | - uses: ./.github/actions/install-dependencies
33 | - run: pnpm -r docs:check
34 | lint:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/checkout@v3
38 | - uses: ./.github/actions/install-dependencies
39 | - run: pnpm -r lint
40 | test-e2e:
41 | runs-on: ubuntu-latest
42 | steps:
43 | - uses: actions/checkout@v3
44 | - uses: ./.github/actions/install-dependencies
45 | - run: pnpm playwright install --with-deps
46 | - run: pnpm -r test:e2e
47 | test-unit:
48 | runs-on: ubuntu-latest
49 | steps:
50 | - uses: actions/checkout@v3
51 | - uses: ./.github/actions/install-dependencies
52 | - run: pnpm -r test:unit
53 | typecheck:
54 | runs-on: ubuntu-latest
55 | steps:
56 | - uses: actions/checkout@v3
57 | - uses: ./.github/actions/install-dependencies
58 | - uses: ./.github/actions/build-core
59 | - run: pnpm typecheck
60 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 | on:
3 | workflow_dispatch:
4 | release:
5 | types: [published]
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | - uses: ./.github/actions/install-dependencies
12 | - uses: ./.github/actions/build-core
13 | - uses: ./.github/actions/build-svelte
14 | - uses: ./.github/actions/build-vue
15 | - run: pnpm config set '//registry.npmjs.org/:_authToken' ${{ secrets.NPM_PUBLISH_TOKEN }}
16 | - run: pnpm publish --access public --no-git-checks
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | dist
4 | node_modules
5 | tmp
6 |
7 | # Playwright
8 | /test-results/
9 | /playwright-report/
10 | /blob-report/
11 | /playwright/.cache/
12 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-workspace-root-check=true
2 | include-workspace-root=true
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "arcanis.vscode-zipfs"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2022 David R. Myers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMeta {
4 | readonly env: ImportMetaEnv
5 | }
6 |
7 | interface ImportMetaEnv {
8 | readonly VITE_SSR: boolean
9 | }
10 |
--------------------------------------------------------------------------------
/examples/demo.ts:
--------------------------------------------------------------------------------
1 | import { ink } from '/src/index'
2 | import example from '/test/assets/example.md?raw'
3 | import { type Instance, type Values } from '/types/ink'
4 |
5 | declare global {
6 | interface Window {
7 | ink: Instance,
8 | // theme helpers
9 | auto: () => void,
10 | dark: () => void,
11 | light: () => void,
12 | }
13 | }
14 |
15 | const url = new URL(window.location.href)
16 | const doc = url.searchParams.get('doc') ?? example
17 |
18 | window.ink = ink(document.getElementById('app')!, {
19 | doc,
20 | files: {
21 | clipboard: true,
22 | dragAndDrop: true,
23 | handler: (files) => {
24 | // eslint-disable-next-line no-console
25 | console.log({ files })
26 |
27 | const lastFile = Array.from(files).pop()
28 |
29 | if (lastFile) {
30 | return URL.createObjectURL(lastFile)
31 | }
32 | },
33 | injectMarkup: true,
34 | },
35 | hooks: {
36 | afterUpdate: (text) => {
37 | url.searchParams.set('doc', text)
38 |
39 | window.history.replaceState(null, '', url)
40 | },
41 | },
42 | interface: {
43 | images: true,
44 | readonly: false,
45 | spellcheck: true,
46 | toolbar: true,
47 | },
48 | lists: true,
49 | placeholder: 'Start typing...',
50 | readability: true,
51 | toolbar: {
52 | upload: true,
53 | },
54 | })
55 |
56 | window.ink.focus()
57 |
58 | const toggleTheme = (theme: Values.Appearance) => {
59 | document.documentElement.classList.remove('auto', 'dark', 'light')
60 | document.documentElement.classList.add(theme)
61 |
62 | window.ink.reconfigure({ interface: { appearance: theme } })
63 | }
64 |
65 | window.auto = toggleTheme.bind(undefined, 'auto')
66 | window.dark = toggleTheme.bind(undefined, 'dark')
67 | window.light = toggleTheme.bind(undefined, 'light')
68 |
69 | toggleTheme('light')
70 |
--------------------------------------------------------------------------------
/examples/hooks.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { mockAll } from '/test/helpers/dom'
3 |
4 | describe('hooks', () => {
5 | it('runs without errors', async () => {
6 | mockAll()
7 |
8 | await expect(import('./hooks')).resolves.not.toThrow()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/examples/hooks.ts:
--------------------------------------------------------------------------------
1 | import { defineOptions, ink } from 'ink-mde'
2 |
3 | // With hooks, you can keep your state in sync with the editor.
4 | const state = { doc: '# Start with some text' }
5 |
6 | // Use defineOptions for automatic type hinting.
7 | const options = defineOptions({
8 | doc: state.doc,
9 | hooks: {
10 | afterUpdate: (doc: string) => {
11 | state.doc = doc
12 | },
13 | },
14 | })
15 |
16 | const editor = ink(document.getElementById('editor')!, options)
17 |
18 | // You can also update the editor directly.
19 | editor.update(state.doc)
20 |
--------------------------------------------------------------------------------
/examples/minimal.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { mockAll } from '/test/helpers/dom'
3 |
4 | describe('minimal', () => {
5 | it('runs without errors', async () => {
6 | mockAll()
7 |
8 | await expect(import('./minimal')).resolves.not.toThrow()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/examples/minimal.ts:
--------------------------------------------------------------------------------
1 | import { ink } from 'ink-mde'
2 |
3 | // The only requirement is an HTML element.
4 | ink(document.getElementById('editor')!)
5 |
--------------------------------------------------------------------------------
/examples/plugins.tsx:
--------------------------------------------------------------------------------
1 | import { ink, plugin } from 'ink-mde'
2 | import { buildBlockWidgetDecoration, buildWidget, nodeDecorator } from '/lib/codemirror-kit'
3 | import { katex } from '/plugins/katex'
4 |
5 | ink(document.querySelector('#app')!, {
6 | doc: '# Start with some text\n\nThis is some \$inline math\$\n\n\$\$\nc = \\pm\\sqrt{a^2 + b^2}\n\$\$\n\n```\nhi\n```\n\n```\nhello\n```',
7 | katex: true,
8 | placeholder: 'This is a really long block of text... This is a really long block of text... This is a really long block of text... This is a really long block of text... This is a really long block of text... This is a really long block of text...',
9 | plugins: [
10 | plugin({
11 | value: async () => {
12 | return nodeDecorator({
13 | nodes: ['FencedCode'],
14 | onMatch: (state, node) => {
15 | const text = state.sliceDoc(node.from, node.to).split('\n').slice(1, -1).join('\n')
16 |
17 | if (text) {
18 | return buildBlockWidgetDecoration({
19 | widget: buildWidget({
20 | // You can see the results of optimization when there is no id specified. Because CodeMirror has no way of knowing
21 | // whether the decoration matches, it usually has to rebuild the DOM on each change. With the optimization setting,
22 | // the DOM only has to be rebuilt if the changes overlap with the existing decoration.
23 | //
24 | // With optimization enabled, the updated timestamp only updates when something inside that block changes.
25 | // With optimization disabled, the updated timestamp updates on *any* doc change.
26 | //
27 | // id: text,
28 | toDOM: () => {
29 | return (
30 |
31 |
DOM updated at: {Date.now()}
32 |
{text}
33 |
34 | ) as HTMLElement
35 | },
36 | }),
37 | })
38 | }
39 | },
40 | // When set to true, only the nodes that overlap changed ranges will be reprocessed.
41 | optimize: true,
42 | })
43 | },
44 | }),
45 | katex(),
46 | ],
47 | })
48 |
--------------------------------------------------------------------------------
/examples/template-ts/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/examples/template-ts/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/template-ts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "template-ts",
3 | "type": "module",
4 | "version": "0.0.0",
5 | "private": true,
6 | "root": true,
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "ink-mde": "workspace:*"
14 | },
15 | "devDependencies": {
16 | "typescript": "^5.3.3",
17 | "vite": "^5.0.10"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/template-ts/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/template-ts/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ink } from 'ink-mde'
2 |
3 | const targetElement = document.getElementById('editor')
4 |
5 | ink(targetElement!, {
6 | doc: 'Hello, world!',
7 | })
8 |
--------------------------------------------------------------------------------
/examples/template-ts/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/template-ts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true
21 | },
22 | "include": ["src"]
23 | }
24 |
--------------------------------------------------------------------------------
/examples/web-component.ts:
--------------------------------------------------------------------------------
1 | import { ink } from 'ink-mde'
2 | import { LitElement, html } from 'lit'
3 |
4 | class InkMde extends LitElement {
5 | firstUpdated() {
6 | ink(this.renderRoot.querySelector('#editor')!, {
7 | // eslint-disable-next-line @typescript-eslint/quotes
8 | doc: "#examples\n\n# A Quick Guide to the Web Crypto API\n\nThe documentation on [MDN](https://developer.mozilla.org/en-US/) is robust, but it requires a lot of jumping around to individual method APIs. I hope this article is helpful for anyone out there looking for guidance.\n\n### Generating a Key\n\nTo start things off, we need to generate a symmetric key.\n\n```js\n// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey\nconst generateKey = async () => {\n return window.crypto.subtle.generateKey({\n name: 'AES-GCM',\n length: 256,\n }, true, ['encrypt', 'decrypt'])\n}\n```\n\n### Encoding Data\n\nBefore we can encrypt data, we first have to encode it into a byte stream. We can achieve this pretty simply with the `TextEncoder` class. This little utility will be used by our `encrypt` function later.\n\n```js\n// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder\nconst encode = (data) => {\n const encoder = new TextEncoder()\n \n return encoder.encode(data)\n}\n```\n\n### Generating an Initialization Vector (IV)\n\nSimply put, an IV is what introduces true randomness into our encryption strategy. When using the same key to encrypt multiple sets of data, it is possible to derive relationships between the encrypted chunks of the cipher and therefore expose some or all of the original message. IVs ensure that repeating character sequences in the input data produce varying byte sequences in the resulting cipher. It is perfectly safe to store IVs in plain text alongside our encrypted message, and we will need to do this to decrypt our message later.\n\n```js\n// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues\nconst generateIv = () => {\n // https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams\n return window.crypto.getRandomValues(new Uint8Array(12))\n}\n```\n\nWe never want to use the same IV with a given key, so it's best to incorporate automatic IV generation into our encryption strategy as we will do later.\n\n### Encrypting Data\n\nNow that we have all of our utilities in place, we can implement our `encrypt` function! As mentioned above, we will need it to return both the cipher _and_ the IV so that we can decrypt the cipher later.\n\n```js\n// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt\nconst encrypt = async (data, key) => {\n const encoded = encode(data)\n const iv = generateIv()\n const cipher = await window.crypto.subtle.encrypt({\n name: 'AES-GCM',\n iv: iv,\n }, key, encoded)\n \n return {\n cipher,\n iv,\n }\n}\n```\n\n## Transmission and Storage\n\nMost practical applications of encryption involve the transmission or storage of said encrypted data. When data is encrypted using SubtleCrypto, the resulting cipher and IV are represented as raw binary data buffers. This is not an ideal format for transmission or storage, so we will tackle packing and unpacking next.\n\n### Packing Data\n\nSince data is often transmitted in JSON and stored in databases, it makes sense to pack our data in a format that is portable. We are going to convert our binary data buffers into base64-encoded strings. Depending on your use case, the base64 encoding is absolutely optional, but I find it helps make the data as portable as you could possibly need.\n\n```js \n// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String\nconst pack = (buffer) => {\n return window.btoa(\n String.fromCharCode.apply(null, new Uint8Array(buffer))\n )\n}\n```\n\n### Unpacking Data\n\nOnce our packed data has been transmitted, stored, and later retrieved, we just need to reverse the process. We will convert our base64-encoded strings back into raw binary buffers.\n\n```js\n// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String\nconst unpack = (packed) => {\n const string = window.atob(packed)\n const buffer = new ArrayBuffer(string.length)\n const bufferView = new Uint8Array(buffer)\n\n for (let i = 0; i < string.length; i++) {\n bufferView[i] = string.charCodeAt(i)\n }\n\n return buffer\n}\n```\n\n## Decryption\n\nWe're in the home stretch! The last step of the process is decrypting our data to see those sweet, sweet secrets. As with unpacking, we just need to reverse the encryption process.\n\n### Decoding Data\n\nAfter decrypting, we will need to decode our resulting byte stream back into its original form. We can achieve this with the `TextDecoder` class. This utility will be used by our `decrypt` function later.\n\n```js\n// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder\nconst decode = (bytestream) => {\n const decoder = new TextDecoder()\n \n return decoder.decode(bytestream)\n}\n```\n\n### Decrypting Data\n\nNow we just need to implement the `decrypt` function. As mentioned before, we will need to supply not just the key but also the IV that was used in the encryption step.\n\n```js\n// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt\nconst decrypt = async (cipher, key, iv) => {\n const encoded = await window.crypto.subtle.decrypt({\n name: 'AES-GCM',\n iv: iv,\n }, key, cipher)\n \n return decode(encoded)\n}\n```\n\n## Putting it into Practice\n\nLet's write an app! Now that all of our utilities are built, we just need to use them. We will encrypt, pack, and transmit our data to a secure endpoint. Then, we will retrieve, unpack, and decrypt the original message.\n\n```js\nconst app = async () => {\n // encrypt message\n const first = 'Hello, World!'\n const key = await generateKey()\n const { cipher, iv } = await encrypt(first, key)\n \n // pack and transmit\n await fetch('/secure-api', {\n method: 'POST',\n body: JSON.stringify({\n cipher: pack(cipher),\n iv: pack(iv),\n }),\n })\n \n // retrieve\n const response = await fetch('/secure-api').then(res => res.json())\n\n // unpack and decrypt message\n const final = await decrypt(unpack(response.cipher), key, unpack(response.iv))\n console.log(final) // logs 'Hello, World!'\n}\n```\n\nThat's all there is to it! We have successfully implemented client-side encryption.\n\nAs a final note, I just want to share [octo](https://github.com/voraciousdev/octo), a note-taking app for developers, one more time. It's free, it's open source, and I would absolutely love it if you checked it out. Thanks, everyone, and happy coding. ✌️",
9 | hooks: {
10 | afterUpdate: (doc) => {
11 | // eslint-disable-next-line no-console
12 | console.log(JSON.stringify(doc))
13 | },
14 | },
15 | })
16 | }
17 |
18 | render() {
19 | return html``
20 | }
21 | }
22 |
23 | customElements.define('ink-mde', InkMde)
24 |
25 | document.querySelector('#app')!.innerHTML = ''
26 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ink-mde
7 |
8 |
9 |
10 |
11 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/lib/codemirror-kit/decorations.ts:
--------------------------------------------------------------------------------
1 | import { syntaxTree } from '@codemirror/language'
2 | import { type EditorState, type Range, type RangeCursor, RangeSet, StateField, type Transaction } from '@codemirror/state'
3 | import { Decoration, EditorView, type WidgetType } from '@codemirror/view'
4 | import { type SyntaxNodeRef } from '@lezer/common'
5 |
6 | // Todo: Maybe open a PR to expose these types.
7 | // https://github.com/codemirror/view/blob/3f1b991f3db20d152045ae9e6872466fc8d8fdac/src/decoration.ts
8 | export type LineDecorationSpec = { attributes?: { [key: string]: string }, class?: string, [other: string]: any }
9 | export type MarkDecorationSpec = { attributes?: { [key: string]: string }, class?: string, inclusive?: boolean, inclusiveEnd?: boolean, inclusiveStart?: boolean, tagName?: string, [other: string]: any }
10 | export type ReplaceDecorationSpec = { block?: boolean, inclusive?: boolean, inclusiveEnd?: boolean, inclusiveStart?: boolean, widget?: WidgetType, [other: string]: any }
11 | export type WidgetDecorationSpec = { widget: WidgetType, block?: boolean, side?: number, [other: string]: any }
12 |
13 | export type Defined = Required<{
14 | [K in keyof T]: NonNullable
15 | }>
16 |
17 | // Custom types.
18 | export type CustomDecorationArgs = Parameters[0]
19 | export type CustomDecoration = T & Decoration
20 | export type CustomDecorationTypes = 'line' | 'mark' | 'replace' | 'widget'
21 | export type CustomWidget = T & WidgetSpec
22 | export type CustomWidgetArgs = {
23 | [K in keyof T]?: K extends 'eq' ? (other: CustomWidget>) => boolean : T[K]
24 | }
25 | export type CustomWidgetOptions = {
26 | [K in keyof T]: K extends 'compare' | 'eq' ? (other: CustomWidget>) => boolean : T[K]
27 | }
28 | export type CustomWidgetDecoration = T & WidgetDecoration & Decoration
29 | export type CustomWidgetDecorationArgs = WidgetDecorationSpec & Record
30 | export type NodeBlockDecoration = CustomWidgetDecoration & { widget: { node: SyntaxNodeRef } }
31 | export type NodeDecoratorArgs = {
32 | nodes: string[],
33 | onMatch: (state: EditorState, node: SyntaxNodeRef) => T | T[] | void,
34 | optimize?: boolean,
35 | range?: {
36 | from?: number,
37 | to?: number,
38 | },
39 | }
40 | export type PartialWidgetSpec = Partial
41 | export type TypedDecoration = Decoration & { spec: Decoration['spec'] & { type: CustomDecorationTypes } }
42 | export type WidgetSpec = WidgetType & { id?: string }
43 | export type WidgetDecoration = { block: boolean, side: number, widget: CustomWidget }
44 |
45 | export const buildBlockWidgetDecoration = (options: T) => {
46 | return buildWidgetDecoration({
47 | block: true,
48 | side: -1,
49 | ...options,
50 | })
51 | }
52 |
53 | export const buildLineDecoration = (options: T) => {
54 | return Decoration.line({
55 | ...options,
56 | type: 'line',
57 | }) as CustomDecoration
58 | }
59 |
60 | export const buildMarkDecoration = (options: T) => {
61 | return Decoration.mark({
62 | ...options,
63 | type: 'mark',
64 | }) as CustomDecoration
65 | }
66 |
67 | export type WidgetOptions> = {
68 | [K in ((keyof T) | 'compare' | 'eq')]?: K extends 'compare' | 'eq' ? (other: WidgetReturn) => boolean
69 | : K extends keyof WidgetSpec ? WidgetSpec[K]
70 | : T[K]
71 | }
72 | export type WidgetReturn> = {
73 | [K in keyof (T & WidgetSpec)]: K extends keyof T ? NonNullable
74 | : K extends keyof WidgetSpec ? WidgetSpec[K]
75 | : never
76 | }
77 |
78 | export const buildWidget = >(options: WidgetOptions): WidgetSpec => {
79 | const eq = (other: WidgetReturn) => {
80 | if (options.eq) return options.eq(other)
81 | if (!options.id) return false
82 |
83 | return options.id === other.id
84 | }
85 |
86 | return {
87 | compare: (other: WidgetReturn) => {
88 | return eq(other)
89 | },
90 | coordsAt: () => null,
91 | destroy: () => {},
92 | eq: (other: WidgetReturn) => {
93 | return eq(other)
94 | },
95 | estimatedHeight: -1,
96 | ignoreEvent: () => true,
97 | lineBreaks: 0,
98 | toDOM: () => {
99 | return document.createElement('span')
100 | },
101 | updateDOM: () => false,
102 | ...options,
103 | }
104 | }
105 |
106 | export const buildWidgetDecoration = (options: T): CustomWidgetDecoration => {
107 | return Decoration.widget({
108 | block: false,
109 | side: 0,
110 | ...options,
111 | widget: buildWidget({
112 | ...options.widget,
113 | }),
114 | type: 'widget',
115 | }) as CustomWidgetDecoration
116 | }
117 |
118 | export const buildNodeDecorations = (state: EditorState, options: NodeDecoratorArgs) => {
119 | const decorationRanges: Range>[] = []
120 |
121 | syntaxTree(state).iterate({
122 | enter: (node) => {
123 | if (options.nodes.includes(node.type.name)) {
124 | const maybeDecorations = options.onMatch(state, node)
125 |
126 | if (!maybeDecorations) return
127 |
128 | const decorations = Array().concat(maybeDecorations)
129 |
130 | decorations.forEach((decoration) => {
131 | if (decoration.spec.type === 'line') {
132 | const wrapped = buildLineDecoration({ ...decoration.spec, node: { ...node } })
133 |
134 | for (let line = state.doc.lineAt(node.from); line.from < node.to; line = state.doc.lineAt(line.to + 1)) {
135 | decorationRanges.push(wrapped.range(line.from))
136 |
137 | if (line.to === state.doc.length) break
138 | }
139 | }
140 |
141 | if (decoration.spec.type === 'mark') {
142 | const wrapped = buildMarkDecoration({ ...decoration.spec, node: { ...node } }).range(node.from, node.to)
143 |
144 | decorationRanges.push(wrapped)
145 | }
146 |
147 | if (decoration.spec.type === 'widget') {
148 | const wrapped = buildWidgetDecoration({ ...decoration.spec, node: { ...node } }).range(node.from)
149 |
150 | decorationRanges.push(wrapped)
151 | }
152 | })
153 | }
154 | },
155 | from: options.range?.from,
156 | to: options.range?.to,
157 | })
158 |
159 | return decorationRanges.sort((left, right) => {
160 | return left.from - right.from
161 | })
162 | }
163 |
164 | export const buildOptimizedNodeDecorations = (rangeSet: RangeSet>, transaction: Transaction, options: NodeDecoratorArgs) => {
165 | const decorations = [] as Range>[]
166 | const cursor = rangeSet.iter()
167 | const cursors = [] as RangeCursor>[]
168 | const cursorsToSkip = [] as RangeCursor>[]
169 |
170 | while (cursor.value) {
171 | cursors.push({ ...cursor })
172 | cursor.next()
173 | }
174 |
175 | transaction.changes.iterChangedRanges((_beforeFrom, _beforeTo, changeFrom, changeTo) => {
176 | cursors.forEach((cursor) => {
177 | if (cursor.value) {
178 | const nodeLength = cursor.value.spec.node.to - cursor.value.spec.node.from
179 | const cursorFrom = cursor.from
180 | const cursorTo = cursor.from + nodeLength
181 |
182 | if (isOverlapping(cursorFrom, cursorTo, changeFrom, changeTo)) {
183 | cursorsToSkip.push(cursor)
184 | }
185 | }
186 | })
187 |
188 | const range = { from: changeFrom, to: changeTo }
189 |
190 | decorations.push(...buildNodeDecorations(transaction.state, { ...options, range }))
191 | })
192 |
193 | const cursorDecos = cursors.filter(cursor => !cursorsToSkip.includes(cursor)).flatMap((cursor) => {
194 | const range = cursor.value?.range(cursor.from) as Range>
195 |
196 | if (!range) return []
197 |
198 | return [range]
199 | })
200 |
201 | decorations.push(...cursorDecos)
202 |
203 | const allDecorations = decorations.sort((left, right) => {
204 | return left.from - right.from
205 | })
206 |
207 | // This reprocesses the entire state.
208 | return allDecorations
209 | }
210 |
211 | export const isOverlapping = (x1: number, x2: number, y1: number, y2: number) => {
212 | return Math.max(x1, y1) <= Math.min(x2, y2)
213 | }
214 |
215 | export const nodeDecorator = (options: NodeDecoratorArgs) => {
216 | return StateField.define>>({
217 | create(state) {
218 | return RangeSet.of(buildNodeDecorations(state, options))
219 | },
220 | update(rangeSet, transaction) {
221 | // Reconfiguration and state effects will reprocess the entire state to ensure nothing is missed.
222 | if (transaction.reconfigured || transaction.effects.length > 0) {
223 | return RangeSet.of(buildNodeDecorations(transaction.state, options))
224 | }
225 |
226 | const updatedRangeSet = rangeSet.map(transaction.changes)
227 |
228 | if (transaction.docChanged) {
229 | // Only process the ranges that are affected by this change.
230 | if (options.optimize) {
231 | return RangeSet.of(buildOptimizedNodeDecorations(updatedRangeSet, transaction, options))
232 | }
233 |
234 | return RangeSet.of(buildNodeDecorations(transaction.state, options))
235 | }
236 |
237 | // No need to redecorate. Instead, just map the decorations through the transaction changes.
238 | return updatedRangeSet
239 | },
240 | provide(field) {
241 | // Provide the extension to the editor.
242 | return EditorView.decorations.from(field)
243 | },
244 | })
245 | }
246 |
--------------------------------------------------------------------------------
/lib/codemirror-kit/index.ts:
--------------------------------------------------------------------------------
1 | export * from './decorations'
2 | export * from './markdown'
3 | export * from './parsers'
4 |
--------------------------------------------------------------------------------
/lib/codemirror-kit/markdown.ts:
--------------------------------------------------------------------------------
1 | import { type Tag, tags } from '@lezer/highlight'
2 | import { type BlockParser, type DelimiterType, type InlineParser, type MarkdownConfig, type NodeSpec } from '@lezer/markdown'
3 | import { buildTag, getCharCode } from './parsers'
4 |
5 | export const buildMarkNode = (markName: string) => {
6 | return buildTaggedNode(markName, [tags.processingInstruction])
7 | }
8 |
9 | export const buildTaggedNode = (nodeName: string, styleTags: Tag[] = []) => {
10 | const tag = buildTag()
11 | const node = defineNode({
12 | name: nodeName,
13 | style: [tag, ...styleTags],
14 | })
15 |
16 | return {
17 | node,
18 | tag,
19 | }
20 | }
21 |
22 | export const defineBlockParser = (options: T) => options
23 | export const defineDelimiter = (options?: T) => ({ ...options })
24 | export const defineInlineParser = (options: T) => options
25 | export const defineMarkdown = (options: T) => options
26 | export const defineNode = (options: T) => options
27 |
28 | /**
29 | * Build an inline Markdown parser that matches a custom expression. E.g. `[[hello]]` for wikilinks.
30 | *
31 | * @param options
32 | * @param options.name The name of the new node type. E.g. `WikiLink` for wikilinks.
33 | * @param options.prefix The tokens that indicate the start of the custom expression. E.g. `[[` for wikilinks.
34 | * @param options.suffix The tokens that indicate the end of the custom expression. E.g. `]]` for wikilinks.
35 | * @param options.matcher The regex that matches the custom expression. E.g. `/(?\[\[)(?.*?)(?\]\])/` for wikilinks.
36 | */
37 | export const buildInlineParser = (options: { name: string, prefix: string, matcher?: RegExp, suffix?: string }) => {
38 | const { name, prefix, suffix } = options
39 | const matcher = options.matcher || new RegExp(`(?${prefix})(?.*?)(?${suffix})`)
40 | const [firstCharCode, ...prefixCharCodes] = prefix.split('').map(getCharCode)
41 |
42 | const taggedNode = buildTaggedNode(name)
43 | const taggedNodeMark = buildMarkNode(`${name}Mark`)
44 | const taggedNodeMarkStart = buildMarkNode(`${name}MarkStart`)
45 | const taggedNodeMarkEnd = buildMarkNode(`${name}MarkEnd`)
46 |
47 | return defineInlineParser({
48 | name: taggedNode.node.name,
49 | parse: (inlineContext, nextCharCode, position) => {
50 | // Match the first char code as efficiently as possible.
51 | if (nextCharCode !== firstCharCode) return -1
52 |
53 | // Check the remaining char codes.
54 | for (let i = 0; i < prefixCharCodes.length; i++) {
55 | const prefixCharCode = prefixCharCodes[i]
56 | const nextCharPosition = position + i + 1
57 |
58 | if (inlineContext.text.charCodeAt(nextCharPosition) !== prefixCharCode) return -1
59 | }
60 |
61 | // Get all the line text left after the current position.
62 | const remainingLineText = inlineContext.slice(position, inlineContext.end)
63 |
64 | if (!matcher.test(remainingLineText)) return -1
65 |
66 | const match = remainingLineText.match(matcher)
67 |
68 | // Todo: Add config to allow empty?
69 | if (!match?.groups?.content) return -1
70 |
71 | const prefixLength = prefix.length
72 | const contentLength = match.groups.content.length
73 | const suffixLength = suffix?.length || 0
74 |
75 | return inlineContext.addElement(
76 | inlineContext.elt(
77 | taggedNode.node.name,
78 | position,
79 | position + prefixLength + contentLength + suffixLength,
80 | [
81 | inlineContext.elt(
82 | taggedNodeMark.node.name,
83 | position,
84 | position + prefixLength,
85 | [
86 | inlineContext.elt(
87 | taggedNodeMarkStart.node.name,
88 | position,
89 | position + prefixLength,
90 | ),
91 | ],
92 | ),
93 | inlineContext.elt(
94 | taggedNodeMark.node.name,
95 | position + prefixLength + contentLength,
96 | position + prefixLength + contentLength + suffixLength,
97 | [
98 | inlineContext.elt(
99 | taggedNodeMarkEnd.node.name,
100 | position + prefixLength + contentLength,
101 | position + prefixLength + contentLength + suffixLength,
102 | ),
103 | ],
104 | ),
105 | ],
106 | ),
107 | )
108 | },
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/lib/codemirror-kit/parsers.ts:
--------------------------------------------------------------------------------
1 | import { Tag } from '@lezer/highlight'
2 |
3 | export const buildTag = (parent?: T) => {
4 | return Tag.define(parent)
5 | }
6 |
7 | export const getCharCode = (char: string) => {
8 | return char.charCodeAt(0)
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ink-mde",
3 | "type": "module",
4 | "version": "0.34.0",
5 | "packageManager": "pnpm@9.1.0+sha256.22e36fba7f4880ecf749a5ca128b8435da085ecd49575e7fb9e64d6bf4fad394",
6 | "description": "A beautiful, modern, customizable Markdown editor powered by CodeMirror 6 and TypeScript.",
7 | "author": "David R. Myers ",
8 | "license": "MIT",
9 | "funding": "https://github.com/sponsors/davidmyersdev",
10 | "homepage": "https://github.com/davidmyersdev/ink-mde",
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/davidmyersdev/ink-mde.git"
14 | },
15 | "bugs": {
16 | "url": "https://github.com/davidmyersdev/ink-mde/issues"
17 | },
18 | "keywords": [
19 | "codemirror",
20 | "component",
21 | "easymde",
22 | "javascript",
23 | "js",
24 | "markdown",
25 | "mde",
26 | "octo",
27 | "sfc",
28 | "simplemde",
29 | "ts",
30 | "typescript",
31 | "vue",
32 | "vue3"
33 | ],
34 | "sideEffects": false,
35 | "exports": {
36 | ".": {
37 | "browser": {
38 | "types": "./dist/client.d.ts",
39 | "require": "./dist/client.cjs",
40 | "import": "./dist/client.js"
41 | },
42 | "node": {
43 | "types": "./dist/index.d.ts",
44 | "require": "./dist/index.cjs",
45 | "import": "./dist/index.js"
46 | },
47 | "types": "./dist/client.d.ts",
48 | "require": "./dist/client.cjs",
49 | "import": "./dist/client.js"
50 | },
51 | "./svelte": {
52 | "types": "./svelte/dist/index.d.ts",
53 | "import": "./svelte/dist/index.js"
54 | },
55 | "./vue": {
56 | "browser": {
57 | "types": "./vue/dist/client.d.ts",
58 | "require": "./vue/dist/client.cjs",
59 | "import": "./vue/dist/client.js"
60 | },
61 | "node": {
62 | "types": "./vue/dist/index.d.ts",
63 | "require": "./vue/dist/index.cjs",
64 | "import": "./vue/dist/index.js"
65 | },
66 | "types": "./vue/dist/client.d.ts",
67 | "require": "./vue/dist/client.cjs",
68 | "import": "./vue/dist/client.js"
69 | }
70 | },
71 | "browser": {
72 | "./dist/index.cjs": "./dist/client.cjs",
73 | "./dist/index.js": "./dist/client.js"
74 | },
75 | "main": "./dist/index.cjs",
76 | "module": "./dist/index.js",
77 | "types": "./dist/index.d.ts",
78 | "files": [
79 | "dist",
80 | "dist/**/*",
81 | "svelte/dist",
82 | "svelte/dist/**/*",
83 | "svelte/package.json",
84 | "vue/dist",
85 | "vue/dist/**/*"
86 | ],
87 | "scripts": {
88 | "build": "run-s core:build && run-p svelte:build vue:build",
89 | "ci": "run-p ci:*",
90 | "ci:build": "run-s build",
91 | "ci:lint": "run-s lint",
92 | "ci:docs": "run-s docs:check",
93 | "ci:dedupe": "pnpm dedupe --use-stderr",
94 | "core:build": "run-s core:clean && run-p core:build:*",
95 | "core:build:client": "vite build",
96 | "core:build:server": "VITE_SSR=1 vite build --ssr",
97 | "core:build:types": "run-s core:typecheck && rollup -c types.config.js",
98 | "core:clean": "rimraf ./dist ./tmp",
99 | "core:dev": "vite",
100 | "core:typecheck": "tsc",
101 | "dev": "run-s core:dev",
102 | "docs:check": "embedme --verify README.md",
103 | "docs:diff": "embedme --stdout README.md",
104 | "docs:update": "embedme README.md",
105 | "lint": "eslint .",
106 | "lint:fix": "eslint --fix .",
107 | "prepack": "run-s build",
108 | "svelte:build": "run-s svelte:clean && pnpm --filter svelte build",
109 | "svelte:clean": "rimraf ./svelte/dist",
110 | "test": "run-s test:watch",
111 | "test:e2e": "playwright test",
112 | "test:unit": "vitest run",
113 | "test:watch": "vitest",
114 | "typecheck": "run-p core:typecheck vue:typecheck",
115 | "vue:build": "run-s vue:clean && run-p vue:typecheck vue:build:*",
116 | "vue:build:client": "vite build -c ./vue/vite.config.ts",
117 | "vue:build:server": "VITE_SSR=1 vite build -c ./vue/vite.config.ts --ssr",
118 | "vue:build:types": "run-s vue:typecheck && rollup -c ./vue/types.config.js",
119 | "vue:clean": "rimraf ./vue/dist",
120 | "vue:dev": "tsx ./vue/server.ts",
121 | "vue:typecheck": "vue-tsc --project ./vue/tsconfig.json"
122 | },
123 | "peerDependencies": {
124 | "svelte": "^3.0.0 || ^4.0.0",
125 | "vue": "^3.0.0"
126 | },
127 | "peerDependenciesMeta": {
128 | "svelte": {
129 | "optional": true
130 | },
131 | "vue": {
132 | "optional": true
133 | }
134 | },
135 | "dependencies": {
136 | "@codemirror/autocomplete": "^6.18.1",
137 | "@codemirror/commands": "^6.6.2",
138 | "@codemirror/lang-markdown": "^6.3.0",
139 | "@codemirror/language": "^6.10.3",
140 | "@codemirror/language-data": "^6.5.1",
141 | "@codemirror/search": "^6.5.6",
142 | "@codemirror/state": "^6.4.1",
143 | "@codemirror/view": "^6.34.1",
144 | "@lezer/common": "^1.2.1",
145 | "@lezer/highlight": "^1.2.1",
146 | "@lezer/markdown": "^1.3.1",
147 | "@replit/codemirror-vim": "^6.2.1",
148 | "katex": "^0.16.9",
149 | "solid-js": "^1.8.7",
150 | "style-mod": "^4.1.2"
151 | },
152 | "devDependencies": {
153 | "@playwright/test": "^1.42.1",
154 | "@rollup/plugin-alias": "^5.1.0",
155 | "@types/express": "^4.17.21",
156 | "@types/katex": "^0.16.7",
157 | "@types/node": "^20.10.6",
158 | "@vitejs/plugin-vue": "^5.0.2",
159 | "@vue/tsconfig": "^0.5.1",
160 | "embedme": "github:davidmyersdev/embedme#live-fork",
161 | "eslint": "^8.56.0",
162 | "eslint-config-artisan": "^0.3.0",
163 | "eslint-plugin-solid": "^0.13.1",
164 | "express": "^4.18.2",
165 | "jsdom": "^23.0.1",
166 | "lit": "^3.1.2",
167 | "npm-run-all": "^4.1.5",
168 | "rimraf": "^5.0.5",
169 | "rollup": "^4.9.2",
170 | "rollup-plugin-dts": "^6.1.0",
171 | "tsx": "^4.7.0",
172 | "typescript": "~5.3.3",
173 | "vite": "^5.0.10",
174 | "vite-plugin-externalize-deps": "^0.8.0",
175 | "vite-plugin-solid": "2.8.0",
176 | "vitest": "^1.1.1",
177 | "vue": "^3.4.3",
178 | "vue-tsc": "^1.8.27"
179 | },
180 | "publishConfig": {
181 | "access": "public"
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test'
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | testDir: './test/e2e',
14 | /* Run tests in files in parallel */
15 | fullyParallel: true,
16 | /* Fail the build on CI if you accidentally left test.only in the source code. */
17 | forbidOnly: !!process.env.CI,
18 | /* Retry on CI only */
19 | retries: process.env.CI ? 2 : 0,
20 | /* Opt out of parallel tests on CI. */
21 | workers: process.env.CI ? 1 : undefined,
22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23 | reporter: 'html',
24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25 | use: {
26 | /* Base URL to use in actions like `await page.goto('/')`. */
27 | baseURL: process.env.BASE_URL || 'http://localhost:5173',
28 |
29 | testIdAttribute: 'data-test-id',
30 |
31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
32 | trace: 'on-first-retry',
33 | video: 'retain-on-failure',
34 | },
35 |
36 | /* Configure projects for major browsers */
37 | projects: [
38 | {
39 | name: 'chromium',
40 | use: { ...devices['Desktop Chrome'] },
41 | },
42 |
43 | {
44 | name: 'firefox',
45 | use: { ...devices['Desktop Firefox'] },
46 | },
47 |
48 | {
49 | name: 'webkit',
50 | use: { ...devices['Desktop Safari'] },
51 | },
52 |
53 | /* Test against mobile viewports. */
54 | // {
55 | // name: 'Mobile Chrome',
56 | // use: { ...devices['Pixel 5'] },
57 | // },
58 | // {
59 | // name: 'Mobile Safari',
60 | // use: { ...devices['iPhone 12'] },
61 | // },
62 |
63 | /* Test against branded browsers. */
64 | // {
65 | // name: 'Microsoft Edge',
66 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
67 | // },
68 | // {
69 | // name: 'Google Chrome',
70 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
71 | // },
72 | ],
73 |
74 | /* Run your local dev server before starting the tests */
75 | webServer: {
76 | command: 'pnpm dev',
77 | url: 'http://localhost:5173',
78 | reuseExistingServer: !process.env.CI,
79 | },
80 | })
81 |
--------------------------------------------------------------------------------
/plugins/katex/grammar.ts:
--------------------------------------------------------------------------------
1 | import { buildMarkNode, buildTaggedNode, defineBlockParser, defineInlineParser, defineMarkdown, getCharCode } from '/lib/codemirror-kit'
2 |
3 | export const charCodes = {
4 | dollarSign: getCharCode('$'),
5 | }
6 |
7 | export const mathInlineTestRegex = /\$.*?\$/
8 | export const mathInlineCaptureRegex = /\$(?