@mdsrs/markdown 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @mdsrs/markdown
2
+
3
+ Safe Markdown, math, and media rendering for `mdsrs` card faces.
4
+
5
+ ## Example
6
+
7
+ ```ts
8
+ import { renderCard, renderMarkdown } from '@mdsrs/markdown';
9
+
10
+ const html = renderMarkdown('The derivative is $2x$.');
11
+ const card = renderCard({
12
+ filePath: 'math/algebra.md',
13
+ frontMarkdown: 'What is $x + x$?',
14
+ backMarkdown: '$2x$'
15
+ });
16
+ ```
17
+
18
+ ## Media
19
+
20
+ Markdown image syntax is reused for media:
21
+
22
+ - `![alt](image.png)` renders an image.
23
+ - `![alt](clip.mp4)` renders a controlled video.
24
+ - `![alt](audio.mp3)` renders a controlled audio player.
25
+
26
+ Use `resolveAsset` to map Markdown URLs to application URLs.
@@ -0,0 +1,18 @@
1
+ export type AssetResolver = (url: string) => string;
2
+ export interface RenderMarkdownOptions {
3
+ macros?: Record<string, string>;
4
+ resolveAsset?: AssetResolver;
5
+ }
6
+ export interface RenderableCard {
7
+ frontMarkdown: string;
8
+ backMarkdown: string;
9
+ }
10
+ export interface RenderedCard {
11
+ frontHtml: string;
12
+ backHtml: string;
13
+ }
14
+ export declare const getMediaExtension: (url: string) => string;
15
+ export declare const parseKatexMacros: (source: string) => Record<string, string>;
16
+ export declare const renderMarkdown: (markdown: string, options?: RenderMarkdownOptions) => string;
17
+ export declare const renderCard: <T extends RenderableCard>(card: T, options?: RenderMarkdownOptions) => T & RenderedCard;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;AAEpD,MAAM,WAAW,qBAAqB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,aAAa,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAiBD,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,WAG5C,CAAC;AAyEF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,2BAW9C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,EAAE,UAAS,qBAA0B,WAmBnF,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,cAAc,EAClD,MAAM,CAAC,EACP,UAAS,qBAA0B,KACjC,CAAC,GAAG,YAIL,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ import rehypeKatex from 'rehype-katex';
2
+ import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
3
+ import rehypeStringify from 'rehype-stringify';
4
+ import remarkMath from 'remark-math';
5
+ import remarkParse from 'remark-parse';
6
+ import remarkRehype from 'remark-rehype';
7
+ import { unified } from 'unified';
8
+ import { visit } from 'unist-util-visit';
9
+ const mediaExtensions = {
10
+ audio: new Set(['.mp3', '.wav', '.ogg', '.m4a', '.flac']),
11
+ video: new Set(['.mp4', '.webm', '.mov', '.m4v'])
12
+ };
13
+ const extensionPattern = /\.[a-z0-9]+(?:[?#].*)?$/i;
14
+ export const getMediaExtension = (url) => {
15
+ const path = url.split(/[?#]/, 1)[0]?.toLowerCase() ?? '';
16
+ return path.match(extensionPattern)?.[0].replace(/[?#].*$/, '') ?? '';
17
+ };
18
+ const remarkMdsrsMedia = (resolveAsset) => (tree) => {
19
+ visit(tree, 'image', (node) => {
20
+ const image = node;
21
+ const originalUrl = typeof image.url === 'string' ? image.url : '';
22
+ const resolvedUrl = resolveAsset(originalUrl);
23
+ const extension = getMediaExtension(originalUrl);
24
+ const alt = typeof image.alt === 'string' ? image.alt : '';
25
+ if (mediaExtensions.audio.has(extension)) {
26
+ image.data = {
27
+ hName: 'audio',
28
+ hProperties: {
29
+ className: ['mdsrs-media', 'mdsrs-media-audio'],
30
+ controls: true,
31
+ src: resolvedUrl
32
+ }
33
+ };
34
+ return;
35
+ }
36
+ if (mediaExtensions.video.has(extension)) {
37
+ image.data = {
38
+ hName: 'video',
39
+ hProperties: {
40
+ className: ['mdsrs-media', 'mdsrs-media-video'],
41
+ controls: true,
42
+ src: resolvedUrl
43
+ }
44
+ };
45
+ return;
46
+ }
47
+ image.url = resolvedUrl;
48
+ image.data = {
49
+ hProperties: {
50
+ alt,
51
+ className: ['mdsrs-media', 'mdsrs-media-image'],
52
+ decoding: 'async',
53
+ loading: 'lazy'
54
+ }
55
+ };
56
+ });
57
+ };
58
+ const schema = {
59
+ ...defaultSchema,
60
+ tagNames: [...(defaultSchema.tagNames ?? []), 'audio', 'video', 'source'],
61
+ attributes: {
62
+ ...defaultSchema.attributes,
63
+ '*': [
64
+ ...(defaultSchema.attributes?.['*'] ?? []),
65
+ 'className',
66
+ ['className', /^mdsrs-/, /^language-/, /^math/, /^katex/]
67
+ ],
68
+ img: [
69
+ ...(defaultSchema.attributes?.img ?? []),
70
+ 'alt',
71
+ 'decoding',
72
+ 'loading',
73
+ 'src',
74
+ 'title'
75
+ ],
76
+ audio: ['className', 'controls', 'src', 'title'],
77
+ video: ['className', 'controls', 'src', 'title']
78
+ },
79
+ protocols: {
80
+ ...defaultSchema.protocols,
81
+ src: [...(defaultSchema.protocols?.src ?? []), 'data']
82
+ }
83
+ };
84
+ export const parseKatexMacros = (source) => {
85
+ const macros = {};
86
+ for (const rawLine of source.split(/\r?\n/)) {
87
+ const line = rawLine.trim();
88
+ if (!line || line.startsWith('%'))
89
+ continue;
90
+ const match = line.match(/^(\\[A-Za-z]+|\\.)\s+(.+)$/);
91
+ if (match)
92
+ macros[match[1] ?? ''] = match[2] ?? '';
93
+ }
94
+ return macros;
95
+ };
96
+ export const renderMarkdown = (markdown, options = {}) => {
97
+ const katexOptions = {
98
+ ...(options.macros ? { macros: options.macros } : {}),
99
+ strict: false,
100
+ trust: false
101
+ };
102
+ return String(unified()
103
+ .use(remarkParse)
104
+ .use(remarkMath)
105
+ .use(remarkMdsrsMedia, options.resolveAsset ?? ((url) => url))
106
+ .use(remarkRehype)
107
+ .use(rehypeSanitize, schema)
108
+ .use(rehypeKatex, katexOptions)
109
+ .use(rehypeStringify)
110
+ .processSync(markdown));
111
+ };
112
+ export const renderCard = (card, options = {}) => ({
113
+ ...card,
114
+ frontHtml: renderMarkdown(card.frontMarkdown, options),
115
+ backHtml: renderMarkdown(card.backMarkdown, options)
116
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@mdsrs/markdown",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "katex": "^0.16.47",
17
+ "rehype-katex": "^7.0.1",
18
+ "rehype-sanitize": "^6.0.0",
19
+ "rehype-stringify": "^10.0.1",
20
+ "remark-math": "^6.0.0",
21
+ "remark-parse": "^11.0.0",
22
+ "remark-rehype": "^11.1.2",
23
+ "unified": "^11.0.5",
24
+ "unist-util-visit": "^5.1.0",
25
+ "@mdsrs/core": "0.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^24.0.0",
29
+ "typescript": "^6.0.3",
30
+ "vitest": "^4.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "test": "vitest run",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit"
36
+ }
37
+ }