mmi-md 1.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/.editorconfig ADDED
@@ -0,0 +1,7 @@
1
+ [*.{html,js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
2
+ charset = utf-8
3
+ indent_size = 4
4
+ indent_style = tab
5
+ insert_final_newline = false
6
+ trim_trailing_whitespace = true
7
+ end_of_line = lf
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "semi": true,
4
+ "singleQuote": true,
5
+ "printWidth": 100
6
+ }
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # mmarkdown
2
+ Markdown spécial MMI
@@ -0,0 +1,77 @@
1
+ export interface DocumentNode {
2
+ type: 'document';
3
+ children: BlockNode[];
4
+ }
5
+ export type BlockNode = HeadingNode | ParagraphNode | ListNode | BlockquoteNode | CodeBlockNode | TableNode;
6
+ export type InlineNode = TextNode | EmphasisNode | StrongNode | UnderlineNode | StrikethroughNode | CodeInlineNode | LinkNode | ImageNode | LineBreakNode;
7
+ export interface TextNode {
8
+ type: 'text';
9
+ content: string;
10
+ }
11
+ export interface EmphasisNode {
12
+ type: 'emphasis';
13
+ children: InlineNode[];
14
+ }
15
+ export interface StrongNode {
16
+ type: 'strong';
17
+ children: InlineNode[];
18
+ }
19
+ export interface UnderlineNode {
20
+ type: 'underline';
21
+ children: InlineNode[];
22
+ }
23
+ export interface StrikethroughNode {
24
+ type: 'strikethrough';
25
+ children: InlineNode[];
26
+ }
27
+ export interface CodeInlineNode {
28
+ type: 'code_inline';
29
+ content: string;
30
+ }
31
+ export interface LinkNode {
32
+ type: 'link';
33
+ text: string;
34
+ href: string;
35
+ external: boolean;
36
+ }
37
+ export interface ImageNode {
38
+ type: 'image';
39
+ alt: string;
40
+ src: string;
41
+ }
42
+ export interface LineBreakNode {
43
+ type: 'line_break';
44
+ }
45
+ export interface HeadingNode {
46
+ type: 'heading';
47
+ level: number;
48
+ subtitle: boolean;
49
+ sample: boolean;
50
+ children: InlineNode[];
51
+ }
52
+ export interface ParagraphNode {
53
+ type: 'paragraph';
54
+ children: InlineNode[];
55
+ }
56
+ export interface BlockquoteNode {
57
+ type: 'blockquote';
58
+ children: BlockNode[];
59
+ }
60
+ export interface ListNode {
61
+ type: 'list';
62
+ ordered: boolean;
63
+ items: ListItemNode[];
64
+ }
65
+ export interface ListItemNode {
66
+ type: 'list_item';
67
+ children: BlockNode[];
68
+ }
69
+ export interface CodeBlockNode {
70
+ type: 'code_block';
71
+ content: string;
72
+ }
73
+ export interface TableNode {
74
+ type: 'table';
75
+ header: InlineNode[][][];
76
+ rows: InlineNode[][][];
77
+ }
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function renderMMarkdown(source: string): string;
package/lib/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { preprocess } from './parser/preprocess.js';
2
+ import { BlockParser } from './parser/BlockParser.js';
3
+ import { HTMLRenderer } from './renderer/HTMLRenderer.js';
4
+ export function renderMMarkdown(source) {
5
+ const cleaned = preprocess(source);
6
+ const lines = cleaned.replace(/\r\n/g, '\n').split('\n');
7
+ const blockParser = new BlockParser();
8
+ const blocks = blockParser.parse(lines);
9
+ const doc = {
10
+ type: 'document',
11
+ children: blocks,
12
+ };
13
+ const renderer = new HTMLRenderer();
14
+ return renderer.render(doc);
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { BlockNode } from '../ast/nodes.js';
2
+ export declare class BlockParser {
3
+ parse(lines: string[]): BlockNode[];
4
+ }
@@ -0,0 +1,137 @@
1
+ import { InlineParser } from './InlineParser.js';
2
+ export class BlockParser {
3
+ parse(lines) {
4
+ const blocks = [];
5
+ let i = 0;
6
+ while (i < lines.length) {
7
+ const line = lines[i];
8
+ if (line.trim() === '') {
9
+ i++;
10
+ continue;
11
+ }
12
+ // Blockquote
13
+ if (line.trim().startsWith('>')) {
14
+ const quote = [];
15
+ while (i < lines.length && lines[i].trim().startsWith('>')) {
16
+ let stripped = lines[i].replace(/^\s*>+\s?/, '');
17
+ quote.push(stripped);
18
+ i++;
19
+ }
20
+ blocks.push({
21
+ type: 'blockquote',
22
+ children: this.parse(quote),
23
+ });
24
+ continue;
25
+ }
26
+ // List (nested)
27
+ const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)/);
28
+ if (listMatch) {
29
+ const indent = listMatch[1].length;
30
+ const ordered = /\d+\./.test(listMatch[2]);
31
+ const items = [];
32
+ while (i < lines.length) {
33
+ const m = lines[i].match(/^(\s*)([-*+]|\d+\.)\s+(.*)/);
34
+ if (!m || m[1].length < indent)
35
+ break;
36
+ const content = m[3];
37
+ const children = [
38
+ {
39
+ type: 'paragraph',
40
+ children: InlineParser.parse(content),
41
+ },
42
+ ];
43
+ i++;
44
+ const sub = [];
45
+ while (i < lines.length && lines[i].startsWith(' '.repeat(indent + 2))) {
46
+ sub.push(lines[i].slice(indent + 2));
47
+ i++;
48
+ }
49
+ children.push(...this.parse(sub));
50
+ items.push({ type: 'list_item', children });
51
+ }
52
+ blocks.push({ type: 'list', ordered, items });
53
+ continue;
54
+ }
55
+ // Heading / Subtitle / Sample
56
+ const h = line.match(/^([~:]?)(#{1,6})\s+(.*)/);
57
+ if (h) {
58
+ blocks.push({
59
+ type: 'heading',
60
+ level: h[2].length,
61
+ subtitle: h[1] === ':',
62
+ sample: h[1] === '~',
63
+ children: InlineParser.parse(h[3]),
64
+ });
65
+ i++;
66
+ continue;
67
+ }
68
+ // Code block
69
+ if (line.startsWith('```')) {
70
+ const content = [];
71
+ i++;
72
+ while (i < lines.length && !lines[i].startsWith('```')) {
73
+ content.push(lines[i]);
74
+ i++;
75
+ }
76
+ i++;
77
+ blocks.push({
78
+ type: 'code_block',
79
+ content: content.join('\n'),
80
+ });
81
+ continue;
82
+ }
83
+ // Table
84
+ if (line.includes('|')) {
85
+ const headerLine = line.split('|').map(c => c.trim()).filter(c => c);
86
+ // Check if next line is a separator
87
+ if (i + 1 < lines.length) {
88
+ const nextLine = lines[i + 1];
89
+ const isSeparator = /^\s*\|?\s*[-:\s|]+\s*\|?\s*$/.test(nextLine) &&
90
+ nextLine.split('|').filter(c => c.trim()).length === headerLine.length;
91
+ if (isSeparator) {
92
+ const header = [headerLine.map(cell => InlineParser.parse(cell))];
93
+ const rows = [];
94
+ i += 2; // Skip header and separator
95
+ // Collect table rows
96
+ while (i < lines.length && lines[i].includes('|')) {
97
+ const rowCells = lines[i].split('|').map(c => c.trim()).filter(c => c);
98
+ if (rowCells.length === headerLine.length) {
99
+ rows.push(rowCells.map(cell => InlineParser.parse(cell)));
100
+ i++;
101
+ }
102
+ else {
103
+ break;
104
+ }
105
+ }
106
+ blocks.push({
107
+ type: 'table',
108
+ header,
109
+ rows,
110
+ });
111
+ continue;
112
+ }
113
+ }
114
+ }
115
+ // Paragraph
116
+ const p = [line];
117
+ i++;
118
+ while (i < lines.length && lines[i].trim() !== '') {
119
+ const nextLine = lines[i];
120
+ // Check if next line starts a new block element
121
+ const isBlockStart = nextLine.trim().startsWith('>') || // blockquote
122
+ nextLine.match(/^(\s*)([-*+]|\d+\.)\s+/) || // list
123
+ nextLine.startsWith('```') || // code block
124
+ nextLine.match(/^([~:]?)(#{1,6})\s+/); // heading
125
+ if (isBlockStart)
126
+ break;
127
+ p.push(nextLine);
128
+ i++;
129
+ }
130
+ blocks.push({
131
+ type: 'paragraph',
132
+ children: InlineParser.parse(p.join('\n')),
133
+ });
134
+ }
135
+ return blocks;
136
+ }
137
+ }
@@ -0,0 +1,5 @@
1
+ import type { InlineNode } from '../ast/nodes.js';
2
+ export declare class InlineParser {
3
+ static parse(text: string): InlineNode[];
4
+ private static mergeText;
5
+ }
@@ -0,0 +1,132 @@
1
+ export class InlineParser {
2
+ static parse(text) {
3
+ const nodes = [];
4
+ let i = 0;
5
+ while (i < text.length) {
6
+ // Backslash escape
7
+ if (text[i] === '\\' && i + 1 < text.length) {
8
+ const nextChar = text[i + 1];
9
+ // Escape special characters
10
+ if ('\\`*_{}[]()#+-.!~|>:'.includes(nextChar)) {
11
+ nodes.push({
12
+ type: 'text',
13
+ content: nextChar,
14
+ });
15
+ i += 2;
16
+ continue;
17
+ }
18
+ }
19
+ // Image
20
+ const img = text.slice(i).match(/^!\[(.*?)\]\((.*?)\)/);
21
+ if (img) {
22
+ nodes.push({
23
+ type: 'image',
24
+ alt: img[1],
25
+ src: img[2],
26
+ });
27
+ i += img[0].length;
28
+ continue;
29
+ }
30
+ // Link
31
+ const link = text.slice(i).match(/^(:?)\[(.*?)\]\((.*?)\)/);
32
+ if (link) {
33
+ nodes.push({
34
+ type: 'link',
35
+ text: link[2],
36
+ href: link[3],
37
+ external: link[1] === ':',
38
+ });
39
+ i += link[0].length;
40
+ continue;
41
+ }
42
+ // Code inline
43
+ if (text[i] === '`') {
44
+ const end = text.indexOf('`', i + 1);
45
+ if (end !== -1) {
46
+ nodes.push({
47
+ type: 'code_inline',
48
+ content: text.slice(i + 1, end),
49
+ });
50
+ i = end + 1;
51
+ continue;
52
+ }
53
+ }
54
+ // Strong
55
+ if (text.startsWith('**', i)) {
56
+ const end = text.indexOf('**', i + 2);
57
+ if (end !== -1) {
58
+ nodes.push({
59
+ type: 'strong',
60
+ children: this.parse(text.slice(i + 2, end)),
61
+ });
62
+ i = end + 2;
63
+ continue;
64
+ }
65
+ }
66
+ // Underline
67
+ if (text.startsWith('__', i)) {
68
+ const end = text.indexOf('__', i + 2);
69
+ if (end !== -1) {
70
+ nodes.push({
71
+ type: 'underline',
72
+ children: this.parse(text.slice(i + 2, end)),
73
+ });
74
+ i = end + 2;
75
+ continue;
76
+ }
77
+ }
78
+ // Strikethrough
79
+ if (text.startsWith('~~', i)) {
80
+ const end = text.indexOf('~~', i + 2);
81
+ if (end !== -1) {
82
+ nodes.push({
83
+ type: 'strikethrough',
84
+ children: this.parse(text.slice(i + 2, end)),
85
+ });
86
+ i = end + 2;
87
+ continue;
88
+ }
89
+ }
90
+ // Italic
91
+ if (text[i] === '*' || text[i] === '_') {
92
+ const end = text.indexOf(text[i], i + 1);
93
+ if (end !== -1) {
94
+ nodes.push({
95
+ type: 'emphasis',
96
+ children: this.parse(text.slice(i + 1, end)),
97
+ });
98
+ i = end + 1;
99
+ continue;
100
+ }
101
+ }
102
+ // Line break
103
+ if (text[i] === '\n') {
104
+ nodes.push({
105
+ type: 'line_break',
106
+ });
107
+ i++;
108
+ continue;
109
+ }
110
+ // Text
111
+ nodes.push({
112
+ type: 'text',
113
+ content: text[i],
114
+ });
115
+ i++;
116
+ }
117
+ return this.mergeText(nodes);
118
+ }
119
+ static mergeText(nodes) {
120
+ const out = [];
121
+ for (const n of nodes) {
122
+ const last = out[out.length - 1];
123
+ if (n.type === 'text' && last?.type === 'text') {
124
+ last.content += n.content;
125
+ }
126
+ else {
127
+ out.push(n);
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ }
@@ -0,0 +1 @@
1
+ export declare function preprocess(source: string): string;
@@ -0,0 +1,3 @@
1
+ export function preprocess(source) {
2
+ return source.replace(/<!--MMDMETA>[\s\S]*?<ENDMETA-->/g, '');
3
+ }
@@ -0,0 +1,6 @@
1
+ import type { DocumentNode } from '../ast/nodes.js';
2
+ export declare class HTMLRenderer {
3
+ render(doc: DocumentNode): string;
4
+ private renderBlock;
5
+ private renderInline;
6
+ }
@@ -0,0 +1,71 @@
1
+ const escapeHTML = (s) => s
2
+ .replace(/&/g, '&amp;')
3
+ .replace(/</g, '&lt;')
4
+ .replace(/>/g, '&gt;')
5
+ .replace(/"/g, '&quot;')
6
+ .replace(/'/g, '&#39;');
7
+ const isAllowedImage = (src) => /\.(png|jpe?g|gif)$/i.test(src);
8
+ export class HTMLRenderer {
9
+ render(doc) {
10
+ return doc.children.map((b) => this.renderBlock(b)).join('\n');
11
+ }
12
+ renderBlock(b) {
13
+ switch (b.type) {
14
+ case 'heading':
15
+ if (b.sample) {
16
+ return `<span class="sample-h${b.level}">${this.renderInline(b.children)}</span>`;
17
+ }
18
+ else if (b.subtitle) {
19
+ return `<span class="subtitle">${this.renderInline(b.children)}</span>`;
20
+ }
21
+ else {
22
+ return `<h${b.level}>${this.renderInline(b.children)}</h${b.level}>`;
23
+ }
24
+ case 'paragraph':
25
+ return `<p>${this.renderInline(b.children)}</p>`;
26
+ case 'blockquote':
27
+ return `<blockquote>${b.children.map((c) => this.renderBlock(c)).join('')}</blockquote>`;
28
+ case 'list':
29
+ const tag = b.ordered ? 'ol' : 'ul';
30
+ return `<${tag}>${b.items.map((i) => `<li>${i.children.map((c) => this.renderBlock(c)).join('')}</li>`).join('')}</${tag}>`;
31
+ case 'code_block':
32
+ return `<pre><code>${escapeHTML(b.content)}</code></pre>`;
33
+ case 'table':
34
+ return `<table>
35
+ <thead>${b.header.map((r) => `<tr>${r.map((c) => `<th>${this.renderInline(c)}</th>`).join('')}</tr>`).join('')}</thead>
36
+ <tbody>${b.rows.map((r) => `<tr>${r.map((c) => `<td>${this.renderInline(c)}</td>`).join('')}</tr>`).join('')}</tbody>
37
+ </table>`;
38
+ default:
39
+ return '';
40
+ }
41
+ }
42
+ renderInline(nodes) {
43
+ return nodes
44
+ .map((n) => {
45
+ switch (n.type) {
46
+ case 'text':
47
+ return escapeHTML(n.content);
48
+ case 'emphasis':
49
+ return `<em>${this.renderInline(n.children)}</em>`;
50
+ case 'strong':
51
+ return `<strong>${this.renderInline(n.children)}</strong>`;
52
+ case 'underline':
53
+ return `<u>${this.renderInline(n.children)}</u>`;
54
+ case 'strikethrough':
55
+ return `<del>${this.renderInline(n.children)}</del>`;
56
+ case 'code_inline':
57
+ return `<code>${escapeHTML(n.content)}</code>`;
58
+ case 'link':
59
+ return `<a href="${escapeHTML(n.href)}"${n.external ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHTML(n.text)}</a>`;
60
+ case 'image':
61
+ if (!isAllowedImage(n.src)) {
62
+ return `<span class="image-error">[image non supportée]</span>`;
63
+ }
64
+ return `<img src="${escapeHTML(n.src)}" alt="${escapeHTML(n.alt)}" loading="lazy" />`;
65
+ case 'line_break':
66
+ return '<br />';
67
+ }
68
+ })
69
+ .join('');
70
+ }
71
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "mmi-md",
3
+ "version": "1.0.0",
4
+ "description": "Markdown créé spécialement pour MMIpedia",
5
+ "keywords": [
6
+ "markdown",
7
+ "mmi"
8
+ ],
9
+ "homepage": "https://github.com/MMI-CODES/mmarkdown#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/MMI-CODES/mmarkdown/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MMI-CODES/mmarkdown.git"
16
+ },
17
+ "license": "UNLICENSED",
18
+ "author": "Loan",
19
+ "type": "module",
20
+ "main": "lib/index.js",
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsx watch src --out-dir lib --clean",
24
+ "start": "tsx lib/index.ts",
25
+ "lint": "eslint . --fix",
26
+ "format": "prettier --write src/"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.0.9",
30
+ "eslint": "^9.39.2",
31
+ "prettier": "^3.8.0",
32
+ "tsx": "^4.21.0",
33
+ "typescript": "^5.9.3"
34
+ }
35
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./lib",
5
+
6
+ "module": "nodenext",
7
+ "target": "esnext",
8
+ "lib": ["esnext"],
9
+ "types": ["node"],
10
+
11
+ "declaration": true,
12
+
13
+ "noUncheckedIndexedAccess": true,
14
+ "exactOptionalPropertyTypes": true,
15
+
16
+ "strict": true,
17
+ "jsx": "react-jsx",
18
+ "verbatimModuleSyntax": true,
19
+ "isolatedModules": true,
20
+ "noUncheckedSideEffectImports": true,
21
+ "moduleDetection": "force",
22
+ "skipLibCheck": true,
23
+ }
24
+ }