mirror of
https://github.com/purocean/yn.git
synced 2026-09-01 15:37:16 +08:00
Merge branch 'release/3.92.1'
This commit is contained in:
@@ -76,14 +76,18 @@ For more information on how to use the following functions, please see [characte
|
||||
|
||||
## Changelogs
|
||||
|
||||
### [v3.90.0](https://github.com/purocean/yn/releases/tag/v3.90.0) 2026-05-16
|
||||
### [v3.92.1](https://github.com/purocean/yn/releases/tag/v3.92.1) 2026-07-12
|
||||
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-win-x64-3.90.0.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-mac-arm64-3.90.0.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-mac-x64-3.90.0.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-linux-x86_64-3.90.0.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-linux-amd64-3.90.0.deb)
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-win-x64-3.92.1.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-mac-arm64-3.92.1.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-mac-x64-3.92.1.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-linux-x86_64-3.92.1.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-linux-amd64-3.92.1.deb)
|
||||
|
||||
1. feat: Add MCP setting inspection tools for reading setting schemas, reading current setting values, and opening the settings panel at a specific key or group
|
||||
2. feat: Add pasted-image filename templates with time and hash placeholders
|
||||
3. feat: Add a CJ-friendly Markdown rendering option for better Chinese/Japanese text emphasis parsing
|
||||
4. feat(editor): Focus the floating editor when clicking preview lines and close it automatically when it loses focus
|
||||
1. feat(markdown): Upgrade Markdown-it to 14.3.0 and add CJK-friendly emphasis parsing for Chinese and Japanese text
|
||||
2. feat(markdown): Add a raw HTML container that can render HTML blocks mixed with Markdown content
|
||||
3. feat(plugin): Add range-based text highlighting APIs for extensions and preview interactions
|
||||
4. feat(editor): Always show the floating editor hint so the feature remains discoverable
|
||||
5. fix(markdown): Avoid double escaping plain-text code fences with a language marker
|
||||
6. fix(editor): Restore HTML and other non-Markdown text files correctly when the app starts
|
||||
7. fix(terminal): Prevent IME layout offsets caused by horizontal terminal overflow
|
||||
8. chore: Upgrade Electron to 41 and update dependencies and security-related transitive packages
|
||||
|
||||
[More release notes](https://github.com/purocean/yn/releases)
|
||||
|
||||
|
||||
+10
-6
@@ -76,14 +76,18 @@
|
||||
|
||||
## 更新日志
|
||||
|
||||
### [v3.90.0](https://github.com/purocean/yn/releases/tag/v3.90.0) 2026-05-16
|
||||
### [v3.92.1](https://github.com/purocean/yn/releases/tag/v3.92.1) 2026-07-12
|
||||
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-win-x64-3.90.0.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-mac-arm64-3.90.0.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-mac-x64-3.90.0.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-linux-x86_64-3.90.0.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.90.0/Yank-Note-linux-amd64-3.90.0.deb)
|
||||
[Windows](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-win-x64-3.92.1.exe) | [macOS arm64](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-mac-arm64-3.92.1.dmg) | [macOS x64](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-mac-x64-3.92.1.dmg) | [Linux AppImage](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-linux-x86_64-3.92.1.AppImage) | [Linux deb](https://github.com/purocean/yn/releases/download/v3.92.1/Yank-Note-linux-amd64-3.92.1.deb)
|
||||
|
||||
1. feat: 增加 MCP 设置排查工具,支持读取设置结构、读取当前设置值,以及打开设置面板并定位到指定字段或分组
|
||||
2. feat: 增加粘贴图片文件名模板,支持时间和哈希占位符
|
||||
3. feat: 增加 CJ-friendly Markdown 渲染选项,优化中文、日文文本中的强调解析
|
||||
4. feat(editor): 点击预览行时聚焦浮动编辑器,并在浮动编辑器失焦后自动关闭
|
||||
1. feat(markdown): Markdown-it 升级到 14.3.0,并增加 CJK-friendly 强调解析,优化中文、日文文本的渲染
|
||||
2. feat(markdown): 增加原始 HTML 容器,支持在 Markdown 内容中混合并直接渲染 HTML 区块
|
||||
3. feat(plugin): 扩展 API 增加基于文本范围的高亮能力,供扩展和预览交互使用
|
||||
4. feat(editor): 始终显示浮动编辑器提示,方便用户发现和使用该功能
|
||||
5. fix(markdown): 修复带语言标记的纯文本代码围栏发生重复转义的问题
|
||||
6. fix(editor): 修复应用启动时无法正确恢复 HTML 及其他非 Markdown 文本文件内容的问题
|
||||
7. fix(terminal): 避免终端横向溢出导致输入法界面发生位置偏移
|
||||
8. chore: Electron 升级到 41,并更新依赖和安全相关的间接依赖
|
||||
|
||||
[更多发布说明](https://github.com/purocean/yn/releases)
|
||||
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yank.note",
|
||||
"version": "3.90.0",
|
||||
"version": "3.92.1",
|
||||
"description": "Yank Note: A highly extensible Markdown editor, designed for productivity.",
|
||||
"main": "dist/main/app.js",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"socks": "^2.8.3",
|
||||
"tar-stream": "^2.2.0",
|
||||
"transliteration": "^2.2.0",
|
||||
"undici": "^6.24.0",
|
||||
"undici": "^6.27.0",
|
||||
"uuid": "^14.0.0",
|
||||
"yaml": "^2.8.3",
|
||||
"yargs": "^15.3.1"
|
||||
@@ -67,7 +67,7 @@
|
||||
"@commitlint/cli": "^16.2.4",
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@electron/notarize": "^2.1.0",
|
||||
"@electron/rebuild": "^4.0.1",
|
||||
"@electron/rebuild": "^4.0.4",
|
||||
"@json-editor/json-editor": "^2.5.4",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/async-lock": "^1.3.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dexie": "^4.0.8",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"electron": "38.7.1",
|
||||
"electron": "41.7.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
@@ -128,11 +128,11 @@
|
||||
"juice": "^8.0.0",
|
||||
"katex": "^0.16.21",
|
||||
"lodash-es": "^4.18.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it": "^14.3.0",
|
||||
"markdown-it-abbr": "^2.0.0",
|
||||
"markdown-it-attributes": "^1.2.0",
|
||||
"markdown-it-cjk-friendly": "2.0.2",
|
||||
"markdown-it-container": "^4.0.0",
|
||||
"markdown-it-cj-friendly": "0.1.1",
|
||||
"markdown-it-emoji": "^3.0.0",
|
||||
"markdown-it-github-alerts": "^0.3.0",
|
||||
"markdown-it-mark": "^4.0.0",
|
||||
@@ -140,6 +140,7 @@
|
||||
"markdown-it-sub": "^2.0.0",
|
||||
"markdown-it-sup": "^2.0.0",
|
||||
"monaco-editor": "^0.49.0",
|
||||
"node-abi": "^4.31.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"parse-author": "^2.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
@@ -153,7 +154,7 @@
|
||||
"typescript": "^5.5.4",
|
||||
"utility-types": "^3.10.0",
|
||||
"viewerjs": "^1.11.6",
|
||||
"vite": "^6.1.6",
|
||||
"vite": "^6.4.3",
|
||||
"vitest": "^4.1.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-tsc": "^2.2.0",
|
||||
|
||||
@@ -196,4 +196,8 @@ export default defineComponent({
|
||||
.xterm-dom ::v-deep(textarea) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.xterm-dom ::v-deep(.xterm-screen) {
|
||||
overflow-x: clip;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -211,7 +211,7 @@ describe('floating-editor plugin', () => {
|
||||
expect(ctx.editorInstance.focus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('watcher shows a limited preview hint when floating editor is available', () => {
|
||||
test('watcher shows a preview hint whenever floating editor is available', () => {
|
||||
const ctx = createCtx()
|
||||
floatingEditor.register(ctx)
|
||||
const canShow = ctx.lib.vue.watch.mock.calls[0][0]
|
||||
@@ -222,7 +222,8 @@ describe('floating-editor plugin', () => {
|
||||
|
||||
const hint = document.body.querySelector('.floating-editor-hint') as HTMLElement
|
||||
expect(hint.textContent).toBe('floating-editor.preview-hint:Alt')
|
||||
expect(ctx.storage.set).toHaveBeenCalledWith('plugin.floating-editor.preview-hint-count', 1)
|
||||
expect(ctx.storage.get).not.toHaveBeenCalled()
|
||||
expect(ctx.storage.set).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('titlebar and resize handles move, maximize, restore, and close the floating frame', async () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const mocks = vi.hoisted(() => ({
|
||||
highlight: vi.fn((code: string, _grammar: any, lang: string) => `<span class="token ${lang}">${code}</span>`),
|
||||
highlight: vi.fn((code: string, _grammar: any, lang: string) => lang === 'text'
|
||||
? code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
: `<span class="token ${lang}">${code}</span>`),
|
||||
juice: vi.fn((html: string) => `<inlined>${html}</inlined>`),
|
||||
loggerWarn: vi.fn(),
|
||||
}))
|
||||
@@ -10,6 +12,7 @@ vi.mock('prismjs', () => ({
|
||||
javascript: {},
|
||||
markup: {},
|
||||
python: {},
|
||||
text: {},
|
||||
typescript: {},
|
||||
},
|
||||
highlight: mocks.highlight,
|
||||
@@ -124,14 +127,22 @@ describe('markdown-code-highlight plugin', () => {
|
||||
const numbered = ctx.md.options.highlight('const a = 1\nconst b = 2\n', 'ts')
|
||||
const escaped = ctx.md.options.highlight('<b>x</b>', '')
|
||||
const fallback = ctx.md.options.highlight('<x>\n', 'unknown')
|
||||
const plainText = ctx.md.options.highlight('http://<host>:7999?install=1\nline2\n', 'text')
|
||||
const plainTextContainer = document.createElement('div')
|
||||
|
||||
plainTextContainer.innerHTML = plainText
|
||||
|
||||
expect(mocks.highlight).toHaveBeenCalledWith('const a = 1\nconst b = 2\n', {}, 'typescript')
|
||||
expect(mocks.highlight).toHaveBeenCalledWith('http://<host>:7999?install=1\nline2\n', {}, 'text')
|
||||
expect(numbered).toContain('<table class="hljs-ln" data-lang="ts">')
|
||||
expect(numbered).toContain('data-line-number="1"')
|
||||
expect(numbered).toContain('data-line-number="2"')
|
||||
expect(numbered).toContain('<span class="token typescript">const a = 1</span>')
|
||||
expect(escaped).toBe('<b>x</b>')
|
||||
expect(fallback).toContain('<x>')
|
||||
expect(plainText).toContain('http://<host>:7999?install=1')
|
||||
expect(plainText).not.toContain('&lt;host')
|
||||
expect(plainTextContainer.querySelector('td.hljs-ln-code .hljs-ln-line')?.textContent).toBe('http://<host>:7999?install=1')
|
||||
expect(mocks.loggerWarn).toHaveBeenCalledWith('Syntax highlight for language "unknown" is not supported.')
|
||||
})
|
||||
|
||||
|
||||
@@ -4,8 +4,12 @@ vi.mock('@fe/context', () => ({
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownContainer from '../markdown-container'
|
||||
import markdownHtml from '../markdown-html'
|
||||
import markdownRenderVNode from '../markdown-render-vnode'
|
||||
|
||||
function createCtx (md: MarkdownIt) {
|
||||
const completionItems: any[] = []
|
||||
|
||||
return {
|
||||
view: { addStyles: vi.fn() },
|
||||
markdown: {
|
||||
@@ -13,9 +17,10 @@ function createCtx (md: MarkdownIt) {
|
||||
},
|
||||
registerHook: vi.fn(),
|
||||
editor: {
|
||||
tapSimpleCompletionItems: vi.fn(),
|
||||
tapSimpleCompletionItems: vi.fn((fn: Function) => fn(completionItems)),
|
||||
tapMarkdownMonarchLanguage: vi.fn((fn: any) => fn({ tokenizer: { root: [] } })),
|
||||
},
|
||||
completionItems,
|
||||
} as any
|
||||
}
|
||||
|
||||
@@ -32,6 +37,7 @@ describe('markdown-container plugin', () => {
|
||||
expect(ctx.registerHook).toHaveBeenCalledWith('VIEW_ON_GET_HTML_FILTER_NODE', expect.any(Function))
|
||||
expect(ctx.editor.tapSimpleCompletionItems).toHaveBeenCalledWith(expect.any(Function))
|
||||
expect(ctx.editor.tapMarkdownMonarchLanguage).toHaveBeenCalledWith(expect.any(Function))
|
||||
expect(ctx.completionItems).toContainEqual(expect.objectContaining({ label: '/ ::: Raw HTML' }))
|
||||
})
|
||||
|
||||
test('renders titled containers as vnodes with escaped titles and parsed attrs', () => {
|
||||
@@ -112,4 +118,70 @@ describe('markdown-container plugin', () => {
|
||||
expect(vnode.children[2].props.class).toBe('group-item-content')
|
||||
expect(vnode.children[2].children[0]).toContain('<pre><code class="language-ts">')
|
||||
})
|
||||
|
||||
test('parses raw html containers without parsing their contents as markdown', () => {
|
||||
const md = new MarkdownIt({ html: true })
|
||||
markdownHtml.register(createCtx(md))
|
||||
markdownRenderVNode.register(createCtx(md))
|
||||
markdownContainer.register(createCtx(md))
|
||||
|
||||
const tokens = md.parse('::: html\n<h1>raw *html*</h1>\n<p>line</p>\n:::', { safeMode: false })
|
||||
const nodes = md.renderer.render(tokens, md.options, { safeMode: false }) as any[]
|
||||
|
||||
expect(tokens.map(token => token.type)).toEqual(['html_block'])
|
||||
expect(tokens[0]).toMatchObject({
|
||||
content: '<h1>raw *html*</h1>\n<p>line</p>\n',
|
||||
map: [1, 3],
|
||||
markup: ':::',
|
||||
})
|
||||
expect(nodes[0].children.map((node: any) => node.type)).toEqual(['h1', 'p'])
|
||||
expect(nodes[0].children[0].props.innerHTML).toBe('raw *html*')
|
||||
})
|
||||
|
||||
test('does not enable raw html containers when html is disabled or safe mode is active', () => {
|
||||
const htmlDisabled = new MarkdownIt({ html: false })
|
||||
const safeMode = new MarkdownIt({ html: true })
|
||||
|
||||
markdownHtml.register(createCtx(htmlDisabled))
|
||||
markdownContainer.register(createCtx(htmlDisabled))
|
||||
markdownHtml.register(createCtx(safeMode))
|
||||
markdownContainer.register(createCtx(safeMode))
|
||||
|
||||
const src = '::: html\n<script>alert(1)</script>\n:::'
|
||||
const disabledTokens = htmlDisabled.parse(src, { safeMode: false })
|
||||
const safeTokens = safeMode.parse(src, { safeMode: true })
|
||||
|
||||
expect(disabledTokens.some(token => token.type === 'html_block')).toBe(false)
|
||||
expect(safeTokens.some(token => token.type === 'html_block')).toBe(false)
|
||||
expect(safeTokens.some(token => token.tag === 'script')).toBe(false)
|
||||
})
|
||||
|
||||
test('uses container fence indentation and marker length rules', () => {
|
||||
const md = new MarkdownIt({ html: true })
|
||||
markdownContainer.register(createCtx(md))
|
||||
|
||||
const tokens = md.parse(':::: html\n<div>\n :::: not a close\n::: also not a close\n</div>\n::::', { safeMode: false })
|
||||
|
||||
expect(tokens).toHaveLength(1)
|
||||
expect(tokens[0]).toMatchObject({
|
||||
type: 'html_block',
|
||||
content: '<div>\n :::: not a close\n::: also not a close\n</div>\n',
|
||||
map: [1, 5],
|
||||
markup: '::::',
|
||||
})
|
||||
})
|
||||
|
||||
test('auto-closes raw html containers at the end of their parent', () => {
|
||||
const md = new MarkdownIt({ html: true })
|
||||
markdownContainer.register(createCtx(md))
|
||||
|
||||
const tokens = md.parse('::: html\n<div>unclosed</div>', { safeMode: false })
|
||||
|
||||
expect(tokens).toHaveLength(1)
|
||||
expect(tokens[0]).toMatchObject({
|
||||
type: 'html_block',
|
||||
content: '<div>unclosed</div>',
|
||||
map: [1, 2],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -544,7 +544,7 @@ describe('markdown extra branch coverage', () => {
|
||||
expect(mocks.replaceLine).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('floating editor noops when unavailable and suppresses hints after the display limit', async () => {
|
||||
test('floating editor noops when unavailable and shows hints once available', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((fn: FrameRequestCallback) => {
|
||||
fn(0)
|
||||
@@ -552,7 +552,6 @@ describe('markdown extra branch coverage', () => {
|
||||
})
|
||||
const ctx = createFloatingCtx()
|
||||
ctx.store.state.showEditor = true
|
||||
ctx.storage.get.mockReturnValue(5)
|
||||
floatingEditor.register(ctx)
|
||||
|
||||
await ctx.actions.get('layout.show-floating-editor').handler({ line: 2, clientY: 80 })
|
||||
@@ -563,6 +562,6 @@ describe('markdown extra branch coverage', () => {
|
||||
expect(canShow()).toBe(false)
|
||||
ctx.store.state.showEditor = false
|
||||
onChange(true)
|
||||
expect(document.body.querySelector('.floating-editor-hint')).toBeNull()
|
||||
expect(document.body.querySelector('.floating-editor-hint')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ const mocks = vi.hoisted(() => ({
|
||||
args: new Map<string, string>(),
|
||||
state: {
|
||||
currentRepo: { name: 'repo-a', path: '/repos/a' } as any,
|
||||
currentFile: null as any,
|
||||
},
|
||||
switchDoc: vi.fn(),
|
||||
whenEditorReady: vi.fn(() => Promise.resolve({})),
|
||||
@@ -80,6 +81,7 @@ describe('status-bar-repository-switch plugin', () => {
|
||||
beforeEach(() => {
|
||||
mocks.args.clear()
|
||||
mocks.state.currentRepo = { name: 'repo-a', path: '/repos/a' }
|
||||
mocks.state.currentFile = null
|
||||
mocks.switchDoc.mockReset()
|
||||
mocks.whenEditorReady.mockReset()
|
||||
mocks.whenEditorReady.mockResolvedValue({})
|
||||
@@ -128,6 +130,57 @@ describe('status-bar-repository-switch plugin', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.each([
|
||||
['Markdown', '/docs/restored.md'],
|
||||
['HTML', '/docs/restored.html'],
|
||||
])('restores the last opened %s file after editor readiness', async (_type, path) => {
|
||||
const ctx = createCtx()
|
||||
mocks.state.currentFile = {
|
||||
type: 'file',
|
||||
repo: 'repo-a',
|
||||
name: path.split('/').pop(),
|
||||
path,
|
||||
}
|
||||
|
||||
repositorySwitch.register(ctx)
|
||||
await mocks.whenEditorReady.mock.results[0].value
|
||||
|
||||
expect(mocks.switchDoc).toHaveBeenCalledWith(mocks.state.currentFile, { force: true })
|
||||
})
|
||||
|
||||
test('does not restore the persisted file when an init file is provided', async () => {
|
||||
const ctx = createCtx()
|
||||
mocks.state.currentFile = {
|
||||
type: 'file',
|
||||
repo: 'repo-a',
|
||||
name: 'restored.html',
|
||||
path: '/docs/restored.html',
|
||||
}
|
||||
mocks.args.set('init-file', '/docs/start.md')
|
||||
|
||||
repositorySwitch.register(ctx)
|
||||
await mocks.whenEditorReady.mock.results[0].value
|
||||
|
||||
expect(mocks.switchDoc).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.switchDoc).toHaveBeenCalledWith(expect.objectContaining({ path: '/docs/start.md' }))
|
||||
})
|
||||
|
||||
test('does not reload a current file that has already finished loading', async () => {
|
||||
const ctx = createCtx()
|
||||
mocks.state.currentFile = {
|
||||
type: 'file',
|
||||
repo: 'repo-a',
|
||||
name: 'restored.md',
|
||||
path: '/docs/restored.md',
|
||||
status: 'loaded',
|
||||
}
|
||||
|
||||
repositorySwitch.register(ctx)
|
||||
await mocks.whenEditorReady.mock.results[0].value
|
||||
|
||||
expect(mocks.switchDoc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('continues startup file switching if init repo selection throws', async () => {
|
||||
const ctx = createCtx()
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
@@ -21,8 +21,6 @@ const DEFAULT_HEIGHT = MIN_HEIGHT
|
||||
const REVEAL_TOP_CONTEXT_LINES = 3
|
||||
const SIDE_MARGIN = 24
|
||||
const SCREEN_MARGIN = 8
|
||||
const HINT_STORAGE_KEY = 'plugin.floating-editor.preview-hint-count'
|
||||
const HINT_LIMIT = 5
|
||||
const HINT_DURATION = 5000
|
||||
const EDITOR_SCROLL_SYNC_PAUSE_TIMEOUT = 350
|
||||
const CLOSE_SYNC_PAUSE_TIMEOUT = 800
|
||||
@@ -272,14 +270,6 @@ export default {
|
||||
editorDom.style.height = `${Math.round(height)}px`
|
||||
}
|
||||
|
||||
function getHintCount () {
|
||||
return ctx.storage.get(HINT_STORAGE_KEY, 0)
|
||||
}
|
||||
|
||||
function setHintCount (count: number) {
|
||||
ctx.storage.set(HINT_STORAGE_KEY, count)
|
||||
}
|
||||
|
||||
function hideHint () {
|
||||
if (hintTimer) {
|
||||
clearTimeout(hintTimer)
|
||||
@@ -305,7 +295,7 @@ export default {
|
||||
}
|
||||
|
||||
function showHint () {
|
||||
if (visible || hint || getHintCount() >= HINT_LIMIT) {
|
||||
if (visible || hint) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -319,7 +309,6 @@ export default {
|
||||
hint.textContent = getHintText()
|
||||
document.body.appendChild(hint)
|
||||
positionHint()
|
||||
setHintCount(getHintCount() + 1)
|
||||
|
||||
hintTimer = window.setTimeout(hideHint, HINT_DURATION)
|
||||
}
|
||||
|
||||
@@ -173,10 +173,6 @@ function duplicateMultilineNodes (element: HTMLElement) {
|
||||
function wrap (code: string, lang: string, lineNumber: boolean) {
|
||||
let html = code
|
||||
|
||||
if (lang === 'text') {
|
||||
html = escape(code)
|
||||
}
|
||||
|
||||
if (lineNumber) {
|
||||
const element = document.createElement('code')
|
||||
element.innerHTML = html
|
||||
@@ -229,7 +225,7 @@ function highlight (str: string, lang: string, lineNumber: boolean) {
|
||||
logger.warn(`Syntax highlight for language "${lang}" is not supported.`)
|
||||
}
|
||||
|
||||
return wrap(str, 'text', lineNumber)
|
||||
return wrap(escape(str), 'text', lineNumber)
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -2,8 +2,83 @@ import MarkdownItContainer from 'markdown-it-container'
|
||||
import { applyAttrs, getAttrs, parseInfo } from 'markdown-it-attributes'
|
||||
import { Fragment, h } from 'vue'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
import type StateBlock from 'markdown-it/lib/rules_block/state_block'
|
||||
import { Plugin } from '@fe/context'
|
||||
|
||||
const RAW_HTML_MARKER = 0x3A // ':'
|
||||
const CONTAINER_NAMES = ['tip', 'warning', 'danger', 'details', 'group-item', 'group', 'row', 'col', 'section', 'div', 'code-group']
|
||||
|
||||
function rawHtmlContainer (state: StateBlock, startLine: number, endLine: number, silent: boolean) {
|
||||
if (!state.md.options.html || (state.env as { safeMode?: boolean })?.safeMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
let start = state.bMarks[startLine] + state.tShift[startLine]
|
||||
let max = state.eMarks[startLine]
|
||||
|
||||
if (state.src.charCodeAt(start) !== RAW_HTML_MARKER) {
|
||||
return false
|
||||
}
|
||||
|
||||
let pos = start + 1
|
||||
while (pos <= max && state.src.charCodeAt(pos) === RAW_HTML_MARKER) {
|
||||
pos++
|
||||
}
|
||||
|
||||
const markerCount = pos - start
|
||||
const params = state.src.slice(pos, max).trim()
|
||||
if (markerCount < 3 || params.split(/\s+/, 1)[0] !== 'html') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
|
||||
let nextLine = startLine
|
||||
let autoClosed = false
|
||||
|
||||
for (;;) {
|
||||
nextLine++
|
||||
if (nextLine >= endLine) {
|
||||
break
|
||||
}
|
||||
|
||||
start = state.bMarks[nextLine] + state.tShift[nextLine]
|
||||
max = state.eMarks[nextLine]
|
||||
|
||||
if (start < max && state.sCount[nextLine] < state.blkIndent) {
|
||||
break
|
||||
}
|
||||
|
||||
if (state.src.charCodeAt(start) !== RAW_HTML_MARKER || state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = start + 1
|
||||
while (pos <= max && state.src.charCodeAt(pos) === RAW_HTML_MARKER) {
|
||||
pos++
|
||||
}
|
||||
|
||||
if (pos - start < markerCount || state.skipSpaces(pos) < max) {
|
||||
continue
|
||||
}
|
||||
|
||||
autoClosed = true
|
||||
break
|
||||
}
|
||||
|
||||
const token = state.push('html_block', '', 0)
|
||||
token.block = true
|
||||
token.markup = ':'.repeat(markerCount)
|
||||
token.info = params
|
||||
token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true)
|
||||
token.map = [startLine + 1, nextLine]
|
||||
|
||||
state.line = nextLine + (autoClosed ? 1 : 0)
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'markdown-container',
|
||||
register: ctx => {
|
||||
@@ -234,7 +309,11 @@ export default {
|
||||
}
|
||||
|
||||
ctx.markdown.registerPlugin(md => {
|
||||
['tip', 'warning', 'danger', 'details', 'group-item', 'group', 'row', 'col', 'section', 'div', 'code-group'].forEach(name => {
|
||||
md.block.ruler.before('fence', 'container_html_raw', rawHtmlContainer, {
|
||||
alt: ['paragraph', 'reference', 'blockquote', 'list']
|
||||
})
|
||||
|
||||
CONTAINER_NAMES.forEach(name => {
|
||||
const reg = new RegExp(`^${name}\\s*(.*)$`)
|
||||
|
||||
md.use(MarkdownItContainer, name, {
|
||||
@@ -346,6 +425,7 @@ export default {
|
||||
|
||||
items.push(
|
||||
{ language: 'markdown', label: '/ ::: Container', insertText: '${3|:::,::::,:::::|} ${1|tip,warning,danger,details,code-group,group,group-item,row,col,section,div|} ${2:Title}\n${4:Content}\n${3|:::,::::,:::::|}\n', block: true, surroundSelection: '${4:Content}' },
|
||||
{ language: 'markdown', label: '/ ::: Raw HTML', insertText: '::: html\n${1:<div>Raw HTML</div>}\n:::\n', block: true, surroundSelection: '${1:<div>Raw HTML</div>}' },
|
||||
{ language: 'markdown', label: '/ ::: Group Container', insertText: ':::: group ${1:Title}\n::: group-item Tab 1\ntest 1\n:::\n::: group-item *Tab 2\ntest 2\n:::\n::: group-item Tab 3\ntest 3\n:::\n::::\n', block: true },
|
||||
{ language: 'markdown', label: '/ ::: Code Group Container', insertText: '::: code-group ${1:Title}\n${2:```js [test.js]\nlet a = 1\n```\n\n```ts [test.ts]\nlet a: number = 1\n```}\n:::\n', block: true },
|
||||
{ language: 'markdown', label: '/ ::: Column Container', insertText: ':::: row ${1:Title}\n::: col\ntest 1\n:::\n::: col\ntest 2\n:::\n::::\n', block: true },
|
||||
|
||||
@@ -23,7 +23,13 @@ export default {
|
||||
}
|
||||
|
||||
if (initFilePath) {
|
||||
switchDoc({ type: 'file', repo: initRepoName || currentRepo!.name, name: basename(initFilePath), path: initFilePath })
|
||||
return switchDoc({ type: 'file', repo: initRepoName || currentRepo!.name, name: basename(initFilePath), path: initFilePath })
|
||||
}
|
||||
|
||||
// The persisted current file only contains metadata. Reload it explicitly
|
||||
// so non-Markdown plain text files also get their content on startup.
|
||||
if (store.state.currentFile && store.state.currentFile.status !== 'loaded') {
|
||||
return switchDoc(store.state.currentFile, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,6 +298,32 @@ describe('document service pure helpers', () => {
|
||||
expect(resolveDocType('/tmp/file.custom')).toBeNull()
|
||||
})
|
||||
|
||||
test('refreshes the current file when runtime document type support changes', async () => {
|
||||
storeMock.state.currentFile = {
|
||||
...fileDoc,
|
||||
path: '/tmp/file.runtime',
|
||||
name: 'file.runtime',
|
||||
}
|
||||
|
||||
registerDocCategory({
|
||||
category: 'runtime-docs',
|
||||
displayName: 'Runtime',
|
||||
types: [{
|
||||
id: 'runtime',
|
||||
displayName: 'Runtime',
|
||||
extension: ['.runtime'],
|
||||
plain: true,
|
||||
}],
|
||||
})
|
||||
await vi.waitFor(() => {
|
||||
expect(apiMocks.readFile).toHaveBeenCalledWith(expect.objectContaining({
|
||||
path: '/tmp/file.runtime',
|
||||
}))
|
||||
})
|
||||
|
||||
removeDocCategory('runtime-docs')
|
||||
})
|
||||
|
||||
test('marks and unmarks documents through settings', async () => {
|
||||
await markDoc(fileDoc)
|
||||
expect(settingMocks.setSetting).toHaveBeenLastCalledWith('mark', [
|
||||
|
||||
@@ -100,16 +100,19 @@ test('tokenize and normalize rules attach source, tokens, and line marks to env'
|
||||
expect(env.eMarks[0]).toBe(7)
|
||||
})
|
||||
|
||||
test('cj friendly plugin can be toggled at render time', async () => {
|
||||
const src = '**这应该被识别为强调。**这是后续文本。'
|
||||
test('cjk friendly plugin can be toggled at render time', async () => {
|
||||
const cases = [
|
||||
['**这应该被识别为强调。**这是后续文本。', '<strong>这应该被识别为强调。</strong>'],
|
||||
['**한국어 괄호 때문에)**후속 문장입니다.', '<strong>한국어 괄호 때문에)</strong>'],
|
||||
]
|
||||
const { markdown } = await loadMarkdown()
|
||||
const env: any = {}
|
||||
|
||||
expect(markdown.render(src, env)).toContain(src)
|
||||
cases.forEach(([src]) => expect(markdown.render(src, env)).toContain(src))
|
||||
|
||||
mocks.settings.set('render.md-cj-friendly', true)
|
||||
expect(markdown.render(src, env)).toContain('<strong>这应该被识别为强调。</strong>')
|
||||
cases.forEach(([src, strong]) => expect(markdown.render(src, env)).toContain(strong))
|
||||
|
||||
mocks.settings.set('render.md-cj-friendly', false)
|
||||
expect(markdown.render(src, env)).toContain(src)
|
||||
cases.forEach(([src]) => expect(markdown.render(src, env)).toContain(src))
|
||||
})
|
||||
|
||||
@@ -1004,6 +1004,7 @@ export function hideHistory () {
|
||||
}
|
||||
|
||||
function cacheSupportedExtension () {
|
||||
const cacheInitialized = supportedExtensionCache.sortedExtensions.length > 0
|
||||
const currentFileSupported = !!(store.state.currentFile && supported(store.state.currentFile))
|
||||
|
||||
supportedExtensionCache.types.clear()
|
||||
@@ -1020,11 +1021,12 @@ function cacheSupportedExtension () {
|
||||
|
||||
supportedExtensionCache.sortedExtensions.sort((a, b) => b.length - a.length)
|
||||
|
||||
// refresh current file if change supported status
|
||||
if (store.state.currentFile) {
|
||||
// Refresh the current file when document types change at runtime. Startup
|
||||
// restoration is handled separately after the editor is ready.
|
||||
if (cacheInitialized && store.state.currentFile) {
|
||||
const currentFileSupportedNow = supported(store.state.currentFile)
|
||||
if (currentFileSupported !== currentFileSupportedNow) {
|
||||
switchDoc(store.state.currentFile || null, { force: true })
|
||||
switchDoc(store.state.currentFile, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import MarkdownItSup from 'markdown-it-sup'
|
||||
import MarkdownItMark from 'markdown-it-mark'
|
||||
import MarkdownItAbbr from 'markdown-it-abbr'
|
||||
import MarkdownItAttributes from 'markdown-it-attributes'
|
||||
import MarkdownItCjFriendly from 'markdown-it-cj-friendly'
|
||||
import MarkdownItCjkFriendly from 'markdown-it-cjk-friendly'
|
||||
import MarkdownItMultimdTable from 'markdown-it-multimd-table'
|
||||
import { triggerHook } from '@fe/core/hook'
|
||||
import { HELP_REPO_NAME } from '@fe/support/args'
|
||||
@@ -18,8 +18,8 @@ import { RULE_NAME as HASHTAG_RULE_NAME } from '@fe/plugins/markdown-hashtags/li
|
||||
*/
|
||||
export const markdown = Markdown({ linkify: true, breaks: true, html: true })
|
||||
const defaultInlineState = markdown.inline.State
|
||||
MarkdownItCjFriendly(markdown)
|
||||
const cjFriendlyInlineState = markdown.inline.State
|
||||
MarkdownItCjkFriendly(markdown)
|
||||
const cjkFriendlyInlineState = markdown.inline.State
|
||||
markdown.inline.State = defaultInlineState
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ markdown.render = (src: string, env: RenderEnv) => {
|
||||
markdown.options.breaks = getSetting('render.md-breaks', true)
|
||||
markdown.options.linkify = getSetting('render.md-linkify', true)
|
||||
markdown.options.typographer = getSetting('render.md-typographer', false)
|
||||
markdown.inline.State = getSetting('render.md-cj-friendly', false) ? cjFriendlyInlineState : defaultInlineState
|
||||
markdown.inline.State = getSetting('render.md-cj-friendly', false) ? cjkFriendlyInlineState : defaultInlineState
|
||||
|
||||
const enabledRules: string[] = []
|
||||
const disabledRules: string[] = []
|
||||
|
||||
@@ -108,9 +108,11 @@ describe('utils index utilities', () => {
|
||||
describe('createTextHighlighter', () => {
|
||||
const highlightStore = new Map<string, unknown>()
|
||||
|
||||
beforeEach(() => {
|
||||
highlightStore.clear()
|
||||
Object.defineProperty(globalThis, 'Highlight', {
|
||||
const installHighlightApi = (target: typeof globalThis, store: Map<string, unknown>) => {
|
||||
const deleteHighlight = vi.fn((name: string) => store.delete(name))
|
||||
const setHighlight = vi.fn((name: string, value: unknown) => store.set(name, value))
|
||||
|
||||
Object.defineProperty(target, 'Highlight', {
|
||||
configurable: true,
|
||||
value: class {
|
||||
ranges: Range[]
|
||||
@@ -120,15 +122,22 @@ describe('utils index utilities', () => {
|
||||
}
|
||||
},
|
||||
})
|
||||
Object.defineProperty(globalThis, 'CSS', {
|
||||
Object.defineProperty(target, 'CSS', {
|
||||
configurable: true,
|
||||
value: {
|
||||
highlights: {
|
||||
delete: vi.fn((name: string) => highlightStore.delete(name)),
|
||||
set: vi.fn((name: string, value: unknown) => highlightStore.set(name, value)),
|
||||
delete: deleteHighlight,
|
||||
set: setHighlight,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return { deleteHighlight, setHighlight }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
highlightStore.clear()
|
||||
installHighlightApi(globalThis, highlightStore)
|
||||
})
|
||||
|
||||
test('installs styles, highlights text matches, and disposes cleanly', () => {
|
||||
@@ -173,5 +182,40 @@ describe('utils index utilities', () => {
|
||||
|
||||
expect((highlightStore.get('words') as { ranges: Range[] }).ranges).toHaveLength(2)
|
||||
})
|
||||
|
||||
test('moves styles and range highlights to the container document', () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
document.body.appendChild(iframe)
|
||||
const iframeWindow = iframe.contentWindow!
|
||||
const iframeDocument = iframe.contentDocument!
|
||||
const iframeHighlightStore = new Map<string, unknown>()
|
||||
const iframeHighlightApi = installHighlightApi(iframeWindow as unknown as typeof globalThis, iframeHighlightStore)
|
||||
const iframeContainer = iframeDocument.createElement('div')
|
||||
iframeContainer.textContent = 'review text'
|
||||
iframeDocument.body.appendChild(iframeContainer)
|
||||
|
||||
let currentContainer = container
|
||||
const highlighter = createTextHighlighter(() => currentContainer, 'review', 'background: yellow')
|
||||
expect(document.querySelector('style[data-highlight-name="review"]')).not.toBeNull()
|
||||
|
||||
const range = iframeDocument.createRange()
|
||||
range.selectNodeContents(iframeContainer)
|
||||
currentContainer = iframeContainer
|
||||
highlighter.highlightRanges([range])
|
||||
|
||||
expect(document.querySelector('style[data-highlight-name="review"]')).toBeNull()
|
||||
expect(iframeDocument.querySelector('style[data-highlight-name="review"]')).not.toBeNull()
|
||||
expect(iframeHighlightApi.setHighlight).toHaveBeenCalledWith('review', expect.any((iframeWindow as any).Highlight))
|
||||
expect((iframeHighlightStore.get('review') as { ranges: Range[] }).ranges).toEqual([range])
|
||||
|
||||
highlighter.dispose()
|
||||
expect(iframeHighlightApi.deleteHighlight).toHaveBeenLastCalledWith('review')
|
||||
expect(iframeDocument.querySelector('style[data-highlight-name="review"]')).toBeNull()
|
||||
iframe.remove()
|
||||
container.remove()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+61
-15
@@ -82,14 +82,22 @@ export function createTextHighlighter (
|
||||
highlightName: string,
|
||||
css: string | undefined | null | ((colorScheme: 'light' | 'dark') => string) = color => `color: ${color === 'dark' ? '#ffec99' : '#bd7f02'}`
|
||||
) {
|
||||
let style: HTMLStyleElement | null = null
|
||||
const resolveContainer = () => typeof container === 'function' ? container() : container
|
||||
|
||||
let style: HTMLStyleElement | null = null
|
||||
let contextDocument = resolveContainer()?.ownerDocument || document
|
||||
let contextWindow = contextDocument.defaultView || window
|
||||
|
||||
const installStyle = () => {
|
||||
if (!css) {
|
||||
return
|
||||
}
|
||||
|
||||
if (css) {
|
||||
// remove existing styles
|
||||
const existingStyle = document.querySelectorAll(`style[data-highlight-name="${highlightName}"]`)
|
||||
const existingStyle = contextDocument.querySelectorAll(`style[data-highlight-name="${highlightName}"]`)
|
||||
existingStyle.forEach(style => style.remove())
|
||||
|
||||
style = document.createElement('style')
|
||||
style = contextDocument.createElement('style')
|
||||
style.dataset.highlightName = highlightName
|
||||
style.textContent = `
|
||||
@media screen {
|
||||
@@ -108,47 +116,84 @@ export function createTextHighlighter (
|
||||
}
|
||||
`
|
||||
|
||||
document.head.appendChild(style)
|
||||
contextDocument.head.appendChild(style)
|
||||
}
|
||||
|
||||
const switchContext = (nextDocument: Document) => {
|
||||
if (nextDocument === contextDocument) {
|
||||
return
|
||||
}
|
||||
|
||||
contextWindow.CSS.highlights.delete(highlightName)
|
||||
style?.remove()
|
||||
style = null
|
||||
contextDocument = nextDocument
|
||||
contextWindow = nextDocument.defaultView || window
|
||||
installStyle()
|
||||
}
|
||||
|
||||
const remove = () => {
|
||||
CSS.highlights.delete(highlightName)
|
||||
contextWindow.CSS.highlights.delete(highlightName)
|
||||
}
|
||||
|
||||
const dispose = () => {
|
||||
remove()
|
||||
style?.remove()
|
||||
style = null
|
||||
}
|
||||
|
||||
const applyRanges = (ranges: Range[], targetDocument: Document) => {
|
||||
switchContext(targetDocument)
|
||||
remove()
|
||||
|
||||
if (ranges.length > 0) {
|
||||
const HighlightConstructor = (contextWindow as any).Highlight
|
||||
contextWindow.CSS.highlights.set(highlightName, new HighlightConstructor(...ranges))
|
||||
}
|
||||
|
||||
return remove
|
||||
}
|
||||
|
||||
/** Highlight precomputed DOM ranges, such as ranges restored from review annotations. */
|
||||
const highlightRanges = (ranges: Range[]) => {
|
||||
const targetDocument = ranges[0]?.startContainer.ownerDocument ||
|
||||
resolveContainer()?.ownerDocument ||
|
||||
contextDocument
|
||||
|
||||
return applyRanges(ranges, targetDocument)
|
||||
}
|
||||
|
||||
const highlight = (keyword: string | RegExp) => {
|
||||
remove()
|
||||
|
||||
keyword = typeof keyword === 'string' ? keyword.trim() : keyword
|
||||
|
||||
if (!keyword) {
|
||||
remove()
|
||||
return
|
||||
}
|
||||
|
||||
const ranges: Range[] = []
|
||||
const containerElement = typeof container === 'function' ? container() : container
|
||||
const containerElement = resolveContainer()
|
||||
|
||||
if (!containerElement) {
|
||||
remove()
|
||||
return
|
||||
}
|
||||
|
||||
const treeWalker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT)
|
||||
switchContext(containerElement.ownerDocument)
|
||||
|
||||
const treeWalker = contextDocument.createTreeWalker(containerElement, contextWindow.NodeFilter.SHOW_TEXT)
|
||||
|
||||
let node: Node | null = null
|
||||
|
||||
do {
|
||||
node = treeWalker.nextNode()
|
||||
if (node && node.nodeType === Node.TEXT_NODE) {
|
||||
if (node && node.nodeType === contextWindow.Node.TEXT_NODE) {
|
||||
const textContent = (node as Text).textContent || ''
|
||||
const regex = typeof keyword === 'string' ? new RegExp(`(${keyword})`, 'gi') : keyword
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = regex.exec(textContent)) !== null) {
|
||||
const range = document.createRange()
|
||||
const range = contextDocument.createRange()
|
||||
range.setStart(node, match.index)
|
||||
range.setEnd(node, match.index + match[0].length)
|
||||
ranges.push(range)
|
||||
@@ -156,14 +201,15 @@ export function createTextHighlighter (
|
||||
}
|
||||
} while (node)
|
||||
|
||||
CSS.highlights.set(highlightName, new Highlight(...ranges))
|
||||
|
||||
return remove
|
||||
return applyRanges(ranges, containerElement.ownerDocument)
|
||||
}
|
||||
|
||||
installStyle()
|
||||
|
||||
return {
|
||||
dispose,
|
||||
remove,
|
||||
highlight,
|
||||
highlightRanges,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ const data = {
|
||||
'md-typographer': 'Enable some language-neutral replacement + quotes beautification',
|
||||
'md-sup': 'Enable sup syntax: 29^th^',
|
||||
'md-sub': 'Enable sub syntax: H~2~O',
|
||||
'md-cj-friendly': 'Enable CJ-friendly emphasis parsing',
|
||||
'md-cj-friendly': 'Enable CJK-friendly emphasis parsing',
|
||||
'md-emoji': 'Enable emoji syntax: :smile:',
|
||||
'list-collapsible': 'Enable collapsible lists - Click to collapse/expand nested lists',
|
||||
'multimd-multiline': '[markdown-it-multimd-table]: Enable multiline syntax',
|
||||
|
||||
@@ -444,7 +444,7 @@ const data: BaseLanguage = {
|
||||
'md-typographer': 'Включите некоторые языковые замены + улучшение кавычек',
|
||||
'md-sup': 'Включить надстрочный синтаксис: 29^th^',
|
||||
'md-sub': 'Включить подстрочный синтаксис: H~2~O',
|
||||
'md-cj-friendly': 'Включить CJ-совместимый разбор выделения',
|
||||
'md-cj-friendly': 'Включить CJK-совместимый разбор выделения',
|
||||
'md-emoji': 'Включите синтаксис эмодзи: :smile:',
|
||||
'list-collapsible': 'Включить складывание списков - Нажмите, чтобы свернуть/развернуть вложенные списки',
|
||||
'multimd-multiline': 'Включить многострочный синтаксис [multimd]:',
|
||||
|
||||
@@ -434,7 +434,7 @@ const data: BaseLanguage = {
|
||||
'md-typographer': '启用排版美化,如 (c) -> ©',
|
||||
'md-sup': '启用上标语法: 29^th^',
|
||||
'md-sub': '启用下标语法: H~2~O',
|
||||
'md-cj-friendly': '启用 CJ 友好的强调解析',
|
||||
'md-cj-friendly': '启用 CJK 友好的强调解析',
|
||||
'md-emoji': '启用 Emoji 语法: :smile:',
|
||||
'list-collapsible': '启用列表折叠 - 点击折叠/展开嵌套列表',
|
||||
'multimd-multiline': '[markdown-it-multimd-table]: 启用 multiline 语法',
|
||||
|
||||
@@ -434,7 +434,7 @@ const data: BaseLanguage = {
|
||||
'md-typographer': '啟用排版美化,如 (c) -> ©',
|
||||
'md-sup': '啟用上標語法: 29^th^',
|
||||
'md-sub': '啟用下標語法: H~2~O',
|
||||
'md-cj-friendly': '啟用 CJ 友好的強調解析',
|
||||
'md-cj-friendly': '啟用 CJK 友好的強調解析',
|
||||
'md-emoji': '啟用 Emoji 語法: :smile:',
|
||||
'list-collapsible': '啟用列表折疊 - 點擊折疊/展開嵌套列表',
|
||||
'multimd-multiline': '[markdown-it-multimd-table]: 啟用 multiline 語法',
|
||||
|
||||
@@ -192,3 +192,15 @@ Code blocks automatically display line numbers with a sticky left panel:
|
||||
// Line 14
|
||||
// Line 15
|
||||
```
|
||||
|
||||
## Plain Text
|
||||
|
||||
```text
|
||||
http://<host>:7999?install=1
|
||||
line2
|
||||
```
|
||||
|
||||
```
|
||||
http://<host>:7999?install=1
|
||||
line2
|
||||
```
|
||||
|
||||
@@ -110,6 +110,17 @@ Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd> to open command palette.
|
||||
|
||||
</div>
|
||||
|
||||
## Raw HTML Container
|
||||
|
||||
The `html` container renders its contents as HTML without parsing Markdown inside it:
|
||||
|
||||
::: html
|
||||
<div style="border: 2px dashed #888; padding: 1em;">
|
||||
<strong>Raw HTML</strong>
|
||||
<span>*This stays literal instead of becoming Markdown emphasis.*</span>
|
||||
</div>
|
||||
:::
|
||||
|
||||
## Notes
|
||||
|
||||
- HTML rendering is controlled by `mdOptions.html` (default: `true`)
|
||||
|
||||
Reference in New Issue
Block a user