Skip to content

Commit 1f7c832

Browse files
committed
refactor: front
1 parent 2dbd6bd commit 1f7c832

12 files changed

Lines changed: 1327 additions & 1252 deletions

File tree

frontend/src/api.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { PLUGIN_ID } from './constants'
2+
3+
export async function canOpen(docId: string, ctx: any = {}) {
4+
const docType = ctx?.document?.type || ctx?.docType
5+
if (docType && docType === 'marp-slide') return true
6+
try {
7+
const kv = await ctx?.host?.api?.getKv?.(PLUGIN_ID, docId, 'meta', ctx?.token)
8+
if (kv && typeof kv === 'object') {
9+
const meta = typeof kv.value === 'object' && kv.value !== null ? kv.value : kv
10+
return Boolean(meta?.isMarp)
11+
}
12+
} catch (err) {
13+
console.warn('[marp] canOpen meta lookup failed', err)
14+
}
15+
return false
16+
}
17+
18+
export async function getRoute(docId: string, ctx: any = {}) {
19+
const token = ctx?.token ? `?token=${encodeURIComponent(ctx.token)}` : ''
20+
return `/marp/${docId}${token}`
21+
}
22+
23+
export async function exec(action: string, { host, payload }: { host: any; payload?: any } = { host: null }) {
24+
const call = host && (host.exec || host.api?.exec)
25+
if (typeof call !== 'function') {
26+
return { ok: false, error: { code: 'EXEC_NOT_AVAILABLE' } }
27+
}
28+
try {
29+
return await call(action, payload || {})
30+
} catch (err: any) {
31+
return { ok: false, error: { code: 'EXEC_ERROR', message: String(err?.message || err) } }
32+
}
33+
}

frontend/src/constants.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { ToolbarAction } from './types'
2+
3+
export const PLUGIN_ID = 'marp'
4+
export const STATE_KEY = 'marpState'
5+
6+
export const DEFAULT_MARKDOWN = `---
7+
marp: true
8+
theme: default
9+
paginate: true
10+
class: lead
11+
---
12+
13+
# Welcome to Marp
14+
15+
- Edit Markdown in the left pane
16+
- Use front-matter directives to configure slides
17+
- Export HTML through the top bar
18+
`
19+
20+
export const STAGE_BASE_CLASS = 'refmd-marp-stage'
21+
22+
export const TOOLBAR_ACTIONS: Array<ToolbarAction | 'divider'> = [
23+
'bold',
24+
'italic',
25+
'heading',
26+
'quote',
27+
'code',
28+
'divider',
29+
'link',
30+
'list',
31+
'list-ordered',
32+
'table',
33+
]
34+
35+
export const TOOLBAR_LABELS: Record<ToolbarAction, string> = {
36+
bold: 'B',
37+
italic: 'I',
38+
heading: 'H',
39+
quote: '"',
40+
code: '<>',
41+
link: '[]',
42+
list: '*',
43+
'list-ordered': '1.',
44+
table: '#|',
45+
}
46+
47+
export const TOOLBAR_TITLES: Record<ToolbarAction, string> = {
48+
bold: 'Bold',
49+
italic: 'Italic',
50+
heading: 'Heading',
51+
quote: 'Block quote',
52+
code: 'Inline code',
53+
link: 'Insert link',
54+
list: 'Bullet list',
55+
'list-ordered': 'Numbered list',
56+
table: 'Table',
57+
}

frontend/src/exporter.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { Kit, UiState } from './types'
2+
3+
export function buildHtmlExport(
4+
state: UiState,
5+
options: { injectPrint?: boolean; forPdf?: boolean } = {},
6+
): string {
7+
const css = state.previewCss || ''
8+
const body = state.previewHtml || '<section class="marp-slide"><h1>Empty slides</h1></section>'
9+
const printScript = options.injectPrint
10+
? '<script>window.addEventListener("load",()=>{try{window.focus();window.print();}catch(e){}});</script>'
11+
: ''
12+
const baseStyles = ['body { margin: 0; }']
13+
if (options.forPdf) {
14+
baseStyles.push('@page { size: 1280px 720px; margin: 0; }')
15+
}
16+
return `<!doctype html>
17+
<html>
18+
<head>
19+
<meta charset="utf-8" />
20+
<title>Marp Slides</title>
21+
<meta name="viewport" content="width=device-width, initial-scale=1" />
22+
<style>
23+
${css}
24+
${baseStyles.join('\n')}
25+
</style>
26+
</head>
27+
<body>
28+
${body}
29+
${printScript}
30+
</body>
31+
</html>`
32+
}
33+
34+
export function exportPdf(state: UiState, kit?: Kit | null) {
35+
if (!state.previewHtml) return
36+
const html = buildHtmlExport(state, { injectPrint: false, forPdf: true })
37+
const iframe = document.createElement('iframe')
38+
iframe.style.position = 'fixed'
39+
iframe.style.width = '0'
40+
iframe.style.height = '0'
41+
iframe.style.border = '0'
42+
iframe.style.opacity = '0'
43+
iframe.style.pointerEvents = 'none'
44+
document.body.appendChild(iframe)
45+
46+
const cleanup = () => {
47+
window.setTimeout(() => {
48+
try {
49+
document.body.removeChild(iframe)
50+
} catch {}
51+
}, 250)
52+
}
53+
54+
const targetWindow = iframe.contentWindow
55+
const targetDoc = targetWindow?.document
56+
if (!targetWindow || !targetDoc) {
57+
kit?.toast?.('error', 'Unable to prepare PDF export frame')
58+
cleanup()
59+
return
60+
}
61+
62+
try {
63+
targetDoc.open()
64+
targetDoc.write(html)
65+
targetDoc.close()
66+
} catch (err) {
67+
console.error('[marp] pdf export failed', err)
68+
kit?.toast?.('error', 'Failed to prepare PDF export')
69+
cleanup()
70+
return
71+
}
72+
73+
const triggerPrint = () => {
74+
try {
75+
targetWindow.focus()
76+
targetWindow.print()
77+
} catch (error) {
78+
console.error('[marp] pdf print failed', error)
79+
kit?.toast?.('error', 'Print dialog could not be opened')
80+
}
81+
cleanup()
82+
}
83+
84+
if (targetDoc.readyState === 'complete') {
85+
window.setTimeout(triggerPrint, 80)
86+
} else {
87+
const onLoad = () => {
88+
targetWindow.removeEventListener('load', onLoad)
89+
window.setTimeout(triggerPrint, 80)
90+
}
91+
targetWindow.addEventListener('load', onLoad)
92+
}
93+
}

frontend/src/header.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { HeaderAction } from './types'
2+
3+
export class HeaderBridge {
4+
constructor(private readonly host: any) {}
5+
6+
setTitle(title?: string | null) {
7+
try {
8+
this.host?.ui?.setDocumentTitle?.(title ?? undefined)
9+
} catch {}
10+
}
11+
12+
setStatus(status?: string | null) {
13+
try {
14+
this.host?.ui?.setDocumentStatus?.(status ?? undefined)
15+
} catch {}
16+
}
17+
18+
setBadge(badge?: string | null) {
19+
try {
20+
this.host?.ui?.setDocumentBadge?.(badge ?? undefined)
21+
} catch {}
22+
}
23+
24+
setActions(actions: HeaderAction[]) {
25+
try {
26+
this.host?.ui?.setDocumentActions?.(actions)
27+
} catch {}
28+
}
29+
}

0 commit comments

Comments
 (0)