Add read-only sharing, API tokens and OpenAPI spec
- Share a note as a live read-only page at /s/{token} with optional password and expiry
- Share page renders with the shared parser (TOC, footnotes, math, Mermaid, code highlighting)
and only serves images from the note's own .assets directory
- Shares follow renames made through the API or externally
- Manage shares and API tokens from the command palette and file tree
- Accept read/write API tokens as Bearer credentials; management endpoints require a session
- Serve a hand-written openapi.json, checked against the route table in tests
This commit is contained in:
1 parent
7b076346a4
commit
5ac01894e6
26 files changed
+3147
-52
No files matched your search
@@ -58,7 +58,7 @@ describe('安全模式', () => {
|
||||
|
||||
describe('Typora 扩展', () => {
|
||||
it('高亮、上下标、脚注引用', () => {
|
||||
expect(renderMarkdown('==a== x^2^ H~2~O[^1]')).toBe('<p><mark>a</mark> x<sup>2</sup> H<sub>2</sub>O<sup class="footnote-ref">1</sup></p>\n')
|
||||
expect(renderMarkdown('==a== x^2^ H~2~O[^1]')).toBe('<p><mark>a</mark> x<sup>2</sup> H<sub>2</sub>O<sup class="footnote-ref"><a href="#fn-1">1</a></sup></p>\n')
|
||||
})
|
||||
it('公式与 emoji 输出待填充的占位元素,内容被转义', () => {
|
||||
expect(renderMarkdown('$a<b$ :smile:')).toBe('<p><span data-math="a<b">a<b</span> <span data-emoji="smile">:smile:</span></p>\n')
|
||||
@@ -67,7 +67,7 @@ describe('Typora 扩展', () => {
|
||||
)
|
||||
})
|
||||
it('脚注定义、front matter 与 [TOC]', () => {
|
||||
expect(renderMarkdown('---\ntitle: x\n---\n[TOC]\n\n[^1]: 说明 **重点**')).toBe('<div class="footnote"><sup>1</sup> 说明 <strong>重点</strong></div>\n')
|
||||
expect(renderMarkdown('---\ntitle: x\n---\n[TOC]\n\n[^1]: 说明 **重点**')).toBe('<nav class="toc"></nav>\n<div class="footnote" id="fn-1"><sup>1</sup> 说明 <strong>重点</strong></div>\n')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ interface LinkDefinition {
|
||||
|
||||
const BLOCK_CONTAINERS = new Set(['Blockquote', 'ListItem'])
|
||||
|
||||
const footnoteId = (label: string) => 'fn-' + label.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
export function escapeHtml(s: string): string {
|
||||
return s.replace(/[&<>"]/g, (c) => (c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : '"'))
|
||||
}
|
||||
@@ -275,8 +277,10 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
const raw = text(node.from, node.to)
|
||||
return `<span data-emoji="${escapeHtml(raw.slice(1, -1))}">${escapeHtml(raw)}</span>`
|
||||
}
|
||||
case 'FootnoteReference':
|
||||
return `<sup class="footnote-ref">${escapeHtml(text(node.from + 2, node.to - 1))}</sup>`
|
||||
case 'FootnoteReference': {
|
||||
const label = text(node.from + 2, node.to - 1)
|
||||
return `<sup class="footnote-ref"><a href="#${escapeAttr(footnoteId(label))}">${escapeHtml(label)}</a></sup>`
|
||||
}
|
||||
case 'InlineCode': {
|
||||
const marks = node.getChildren('CodeMark')
|
||||
let code = text(marks[0].to, marks[marks.length - 1].from).replace(/\n/g, ' ')
|
||||
@@ -439,9 +443,13 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
||||
}
|
||||
case 'FootnoteDefinition': {
|
||||
const label = node.getChild('FootnoteLabel')!
|
||||
return `<div class="footnote"><sup>${escapeHtml(text(label.from + 2, label.to - 2))}</sup> ${inline(node, label.to, node.to).trim()}</div>\n`
|
||||
const name = text(label.from + 2, label.to - 2)
|
||||
return `<div class="footnote" id="${escapeAttr(footnoteId(name))}"><sup>${escapeHtml(name)}</sup> ${inline(node, label.to, node.to).trim()}</div>\n`
|
||||
}
|
||||
// front matter 是元数据,不输出;[TOC] 在只读分享页面(M4)中生成
|
||||
// 目录占位,由分享页根据标题填充
|
||||
case 'TableOfContents':
|
||||
return '<nav class="toc"></nav>\n'
|
||||
// front matter 是元数据,不输出
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user