sommark 2.3.2 → 3.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 +47 -42
- package/SOMMARK-SPEC.md +483 -0
- package/cli/cli.mjs +42 -2
- package/cli/commands/color.js +36 -0
- package/cli/commands/help.js +7 -0
- package/cli/commands/init.js +2 -0
- package/cli/commands/list.js +119 -0
- package/cli/commands/print.js +61 -11
- package/cli/commands/show.js +24 -27
- package/cli/constants.js +1 -1
- package/cli/helpers/config.js +14 -4
- package/cli/helpers/transpile.js +27 -32
- package/constants/html_props.js +100 -0
- package/constants/html_tags.js +146 -0
- package/constants/void_elements.js +26 -0
- package/core/lexer.js +70 -39
- package/core/parser.js +100 -84
- package/core/pluginManager.js +139 -0
- package/core/plugins/comment-remover.js +47 -0
- package/core/plugins/module-system.js +137 -0
- package/core/plugins/quote-escaper.js +37 -0
- package/core/plugins/raw-content-plugin.js +72 -0
- package/core/plugins/rules-validation-plugin.js +197 -0
- package/core/plugins/sommark-format.js +211 -0
- package/core/transpiler.js +65 -198
- package/debug.js +9 -4
- package/format.js +23 -0
- package/formatter/mark.js +3 -3
- package/formatter/tag.js +6 -2
- package/grammar.ebnf +5 -5
- package/helpers/camelize.js +2 -0
- package/helpers/colorize.js +20 -14
- package/helpers/kebabize.js +2 -0
- package/helpers/utils.js +161 -0
- package/index.js +243 -44
- package/mappers/languages/html.js +200 -105
- package/mappers/languages/json.js +23 -4
- package/mappers/languages/markdown.js +88 -67
- package/mappers/languages/mdx.js +130 -2
- package/mappers/mapper.js +77 -246
- package/package.json +7 -5
- package/unformatted.smark +90 -0
- package/v3-todo.smark +75 -0
- package/CHANGELOG.md +0 -119
- package/helpers/loadCss.js +0 -46
package/index.js
CHANGED
|
@@ -12,70 +12,265 @@ import { runtimeError } from "./core/errors.js";
|
|
|
12
12
|
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat } from "./core/formats.js";
|
|
13
13
|
import TOKEN_TYPES from "./core/tokenTypes.js";
|
|
14
14
|
import * as labels from "./core/labels.js";
|
|
15
|
+
import PluginManager from "./core/pluginManager.js";
|
|
16
|
+
import QuoteEscaper from "./core/plugins/quote-escaper.js";
|
|
17
|
+
import ModuleSystem from "./core/plugins/module-system.js";
|
|
18
|
+
import RawContentPlugin from "./core/plugins/raw-content-plugin.js";
|
|
19
|
+
import CommentRemover from "./core/plugins/comment-remover.js";
|
|
20
|
+
import RulesValidationPlugin from "./core/plugins/rules-validation-plugin.js";
|
|
21
|
+
import SomMarkFormat from "./core/plugins/sommark-format.js";
|
|
22
|
+
import { enableColor } from "./helpers/colorize.js";
|
|
23
|
+
import { htmlTable, list, parseList, safeArg, todo } from "./helpers/utils.js";
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export const BUILT_IN_PLUGINS = [QuoteEscaper, ModuleSystem, RawContentPlugin, CommentRemover, RulesValidationPlugin, SomMarkFormat];
|
|
27
|
+
|
|
15
28
|
class SomMark {
|
|
16
|
-
constructor({ src, format, mapperFile = null, includeDocument = true }) {
|
|
29
|
+
constructor({ src, format, mapperFile = null, includeDocument = true, plugins = [], excludePlugins = [], priority = [] }) {
|
|
17
30
|
this.src = src;
|
|
18
31
|
this.format = format;
|
|
19
32
|
this.mapperFile = mapperFile;
|
|
33
|
+
this.priority = priority;
|
|
34
|
+
|
|
35
|
+
// 1. Identify which built-in plugins should be active by default
|
|
36
|
+
// For now, QuoteEscaper is active, others are inactive (require manual activation)
|
|
37
|
+
const inactiveByDefault = ["raw-content", "sommark-format"];
|
|
38
|
+
let activeBuiltIns = BUILT_IN_PLUGINS.filter(p =>
|
|
39
|
+
!inactiveByDefault.includes(p.name) && !excludePlugins.includes(p.name)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 2. Process 'plugins' array:
|
|
43
|
+
// - If string, look up in BUILT_IN_PLUGINS
|
|
44
|
+
// - If object with { name, options }, it's a built-in override
|
|
45
|
+
// - If object with { plugin, options }, it's an external override
|
|
46
|
+
// - If object without name/plugin but with other keys, it's a direct plugin object
|
|
47
|
+
let processedPlugins = [];
|
|
48
|
+
let manuallyActivatedNames = [];
|
|
49
|
+
|
|
50
|
+
plugins.forEach(p => {
|
|
51
|
+
if (typeof p === "string") {
|
|
52
|
+
const builtIn = BUILT_IN_PLUGINS.find(bp => bp.name === p);
|
|
53
|
+
if (builtIn) {
|
|
54
|
+
processedPlugins.push({ ...builtIn }); // Clone to avoid mutation
|
|
55
|
+
manuallyActivatedNames.push(p);
|
|
56
|
+
}
|
|
57
|
+
} else if (typeof p === "object" && p !== null) {
|
|
58
|
+
if (p.name && p.options && !p.type) {
|
|
59
|
+
// Built-in Override: { name: "raw-content", options: { ... } }
|
|
60
|
+
const builtIn = BUILT_IN_PLUGINS.find(bp => bp.name === p.name);
|
|
61
|
+
if (builtIn) {
|
|
62
|
+
processedPlugins.push({
|
|
63
|
+
...builtIn,
|
|
64
|
+
options: { ...builtIn.options, ...p.options }
|
|
65
|
+
});
|
|
66
|
+
manuallyActivatedNames.push(p.name);
|
|
67
|
+
}
|
|
68
|
+
} else if (p.plugin && p.options) {
|
|
69
|
+
// External Override: { plugin: myPlugin, options: { ... } }
|
|
70
|
+
processedPlugins.push({
|
|
71
|
+
...p.plugin,
|
|
72
|
+
options: { ...p.plugin.options, ...p.options }
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// Direct Plugin Object
|
|
76
|
+
processedPlugins.push(p);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 3. Merge: Default active built-ins (minus ones manually re-added) + Processed Plugins
|
|
82
|
+
const finalPlugins = [
|
|
83
|
+
...activeBuiltIns
|
|
84
|
+
.filter(p => !manuallyActivatedNames.includes(p.name))
|
|
85
|
+
.map(p => ({ ...p })), // Clone defaults for isolation
|
|
86
|
+
...processedPlugins
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
this.plugins = finalPlugins;
|
|
90
|
+
this.pluginManager = new PluginManager(this.plugins, this.priority);
|
|
20
91
|
|
|
21
92
|
this.Mapper = Mapper;
|
|
22
93
|
this.includeDocument = includeDocument;
|
|
23
|
-
|
|
94
|
+
|
|
95
|
+
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json, [textFormat]: new Mapper() };
|
|
96
|
+
|
|
97
|
+
if (!this.mapperFile && this.format) {
|
|
98
|
+
const DefaultMapper = mapperFiles[this.format];
|
|
99
|
+
if (DefaultMapper) {
|
|
100
|
+
this.mapperFile = DefaultMapper.clone();
|
|
101
|
+
}
|
|
102
|
+
} else if (this.mapperFile) {
|
|
103
|
+
this.mapperFile = this.mapperFile.clone();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this._initializeMappers();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
register = (id, render, options) => {
|
|
110
|
+
this.mapperFile.register(id, render, options);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
inherit = (...mappers) => {
|
|
114
|
+
this.mapperFile.inherit(...mappers);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
get = id => {
|
|
118
|
+
return this.mapperFile.get(id);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
removeOutput = id => {
|
|
122
|
+
this.mapperFile.removeOutput(id);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
clear = () => {
|
|
126
|
+
this.mapperFile.clear();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
_initializeMappers() {
|
|
130
|
+
// 1. Check if a plugin provides a mapper for this format
|
|
131
|
+
const pluginMapper = this.pluginManager.getFormatMapper(this.format);
|
|
132
|
+
if (pluginMapper) {
|
|
133
|
+
this.mapperFile = pluginMapper.clone();
|
|
134
|
+
}
|
|
135
|
+
|
|
24
136
|
if (!this.format) {
|
|
25
137
|
runtimeError(["{line}<$red:Undefined Format$>: <$yellow:Format argument is not defined.$>{line}"]);
|
|
26
138
|
}
|
|
27
|
-
|
|
28
|
-
runtimeError([
|
|
29
|
-
`{line}<$red:Unknown Format$>: <$yellow:You provided unknown format:$> <$green:'${format}'$>`,
|
|
30
|
-
`{N}<$yellow:Accepted formats are:$> [<$cyan: ${accepted_formats.join(", ")}$>]{line}`
|
|
31
|
-
]);
|
|
32
|
-
}
|
|
33
|
-
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json};
|
|
139
|
+
|
|
34
140
|
if (!this.mapperFile && this.format) {
|
|
35
|
-
|
|
141
|
+
runtimeError([`{line}<$red:Unknown Format$>: <$yellow:Mapper for format '${this.format}' not found.$>{line}`]);
|
|
36
142
|
}
|
|
37
143
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
144
|
+
|
|
145
|
+
async _applyScopedPreprocessors(src) {
|
|
146
|
+
let processed = await this.pluginManager.runPreprocessor(src, "top-level");
|
|
147
|
+
|
|
148
|
+
// Helper for async regex replacement
|
|
149
|
+
const asyncReplace = async (str, regex, scope) => {
|
|
150
|
+
if (typeof str !== "string") return str;
|
|
151
|
+
const matches = [...str.matchAll(regex)];
|
|
152
|
+
if (matches.length === 0) return str;
|
|
153
|
+
|
|
154
|
+
// Process all matches in parallel for efficiency
|
|
155
|
+
const replacements = await Promise.all(
|
|
156
|
+
matches.map(async match => {
|
|
157
|
+
// match[2] is the group for content inside quotes/brackets/whatever depending on the regex
|
|
158
|
+
let contentToProcess;
|
|
159
|
+
if (scope === "arguments") contentToProcess = match[2];
|
|
160
|
+
if (scope === "content") contentToProcess = match[2];
|
|
161
|
+
|
|
162
|
+
if (contentToProcess !== undefined) {
|
|
163
|
+
const processedContent = await this.pluginManager.runPreprocessor(contentToProcess, scope);
|
|
164
|
+
// Reconstruct the match with processed content
|
|
165
|
+
if (scope === "arguments") return match[0].replace(match[2], processedContent);
|
|
166
|
+
if (scope === "content") return match[0].replace(match[2], processedContent);
|
|
167
|
+
}
|
|
168
|
+
return match[0];
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Reconstruct string by replacing matches in order
|
|
173
|
+
let i = 0;
|
|
174
|
+
return str.replace(regex, () => replacements[i++]);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// 1. Process Arguments Scope [...]
|
|
178
|
+
const argRegex = /\[\s*([a-zA-Z0-9\-_$]+)\s*(?:=\s*((?:[^"\\\]]|\\[\s\S]|"[^"]*")*))?\s*\]/g;
|
|
179
|
+
processed = await asyncReplace(processed, argRegex, "arguments");
|
|
180
|
+
|
|
181
|
+
// 2. Process Content Scope
|
|
182
|
+
const contentRegex = /(\]\s*)([\s\S]*?)(\s*\[\s*end\s*\])/g;
|
|
183
|
+
processed = await asyncReplace(processed, contentRegex, "content");
|
|
184
|
+
|
|
185
|
+
return processed;
|
|
49
186
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
187
|
+
|
|
188
|
+
async lex(src = this.src) {
|
|
189
|
+
if (src !== this.src) this.src = src;
|
|
190
|
+
const processedSrc = await this._applyScopedPreprocessors(this.src);
|
|
191
|
+
let tokens = lexer(processedSrc);
|
|
192
|
+
tokens = await this.pluginManager.runAfterLex(tokens);
|
|
193
|
+
return tokens;
|
|
53
194
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
195
|
+
|
|
196
|
+
async parse(src = this.src) {
|
|
197
|
+
const tokens = await this.lex(src);
|
|
198
|
+
let ast = parser(tokens);
|
|
199
|
+
ast = await this.pluginManager.runOnAst(ast, { mapperFile: this.mapperFile });
|
|
200
|
+
return ast;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async transpile(src = this.src) {
|
|
204
|
+
if (src !== this.src) this.src = src;
|
|
205
|
+
// 1. Resolve Dynamic Formats from Plugins if built-in failed
|
|
206
|
+
if (!this.mapperFile) {
|
|
207
|
+
const PluginMapper = this.pluginManager.getFormatMapper(this.format);
|
|
208
|
+
if (PluginMapper) {
|
|
209
|
+
this.mapperFile = PluginMapper.clone ? PluginMapper.clone() : PluginMapper;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Final check
|
|
214
|
+
if (!this.mapperFile) {
|
|
215
|
+
runtimeError([
|
|
216
|
+
`{line}<$red:Unknown Format$>: <$yellow:No mapper found for format:$> <$green:'${this.format}'$>`,
|
|
217
|
+
`{N}<$yellow:Make sure you have registered a plugin that provides this format.$>{line}`
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Run active registration hooks from plugins
|
|
222
|
+
this.pluginManager.runRegisterHooks(this);
|
|
223
|
+
|
|
224
|
+
const ast = await this.parse(src);
|
|
225
|
+
|
|
226
|
+
// 2. Extend Mapper with static plugins definitions
|
|
227
|
+
const extensions = this.pluginManager.getMapperExtensions();
|
|
228
|
+
if (extensions.outputs.length > 0) {
|
|
229
|
+
for (const out of extensions.outputs) {
|
|
230
|
+
// Support both object {id, render, options} and array [id, render, options]
|
|
231
|
+
if (Array.isArray(out)) {
|
|
232
|
+
const [id, render, options = {}] = out;
|
|
233
|
+
this.register(id, render, options);
|
|
234
|
+
} else if (typeof out === "object" && out !== null) {
|
|
235
|
+
const renderFn = out.register || out.render;
|
|
236
|
+
if (typeof renderFn === "function") {
|
|
237
|
+
this.register(out.id, renderFn, out.options || {});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Add recognized arguments if provided by plugins
|
|
244
|
+
if (extensions.rules && extensions.rules.recognizedArguments) {
|
|
245
|
+
if (Array.isArray(extensions.rules.recognizedArguments)) {
|
|
246
|
+
extensions.rules.recognizedArguments.forEach(arg => this.mapperFile.extraProps.add(arg));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let result = await transpiler({ ast, format: this.format, mapperFile: this.mapperFile, includeDocument: this.includeDocument });
|
|
251
|
+
|
|
252
|
+
// 3. Run Transformers
|
|
253
|
+
return await this.pluginManager.runTransformers(result);
|
|
57
254
|
}
|
|
58
255
|
}
|
|
59
256
|
|
|
60
|
-
const lex = src =>
|
|
257
|
+
const lex = async (src, plugins = [], excludePlugins = []) => {
|
|
258
|
+
return await new SomMark({ src, plugins, format: htmlFormat, excludePlugins }).lex();
|
|
259
|
+
};
|
|
61
260
|
|
|
62
|
-
function parse(src) {
|
|
261
|
+
async function parse(src, plugins = [], excludePlugins = []) {
|
|
63
262
|
if (!src) {
|
|
64
263
|
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for parsing.$>{line}`]);
|
|
65
264
|
}
|
|
66
|
-
|
|
67
|
-
if (!Array.isArray(tokens) || tokens.length === 0) {
|
|
68
|
-
runtimeError([`{line}<$red:Invalid tokens:$> <$yellow:Expecting a non-empty array of tokens.$>{line}`]);
|
|
69
|
-
}
|
|
70
|
-
return parser(tokens);
|
|
265
|
+
return await new SomMark({ src, plugins, format: htmlFormat, excludePlugins }).parse();
|
|
71
266
|
}
|
|
72
267
|
|
|
73
268
|
async function transpile(options = {}) {
|
|
74
|
-
const { src, format = htmlFormat, mapperFile =
|
|
269
|
+
const { src, format = htmlFormat, mapperFile = null, includeDocument = true, plugins = [], excludePlugins = [], priority = [] } = options;
|
|
75
270
|
if (typeof options !== "object" || options === null) {
|
|
76
271
|
runtimeError([`{line}<$red:Invalid Options:$> <$yellow:The options argument must be a non-null object.$>{line}`]);
|
|
77
272
|
}
|
|
78
|
-
const knownProps = ["src", "format", "mapperFile", "includeDocument"];
|
|
273
|
+
const knownProps = ["src", "format", "mapperFile", "includeDocument", "plugins", "excludePlugins", "priority"];
|
|
79
274
|
Object.keys(options).forEach(key => {
|
|
80
275
|
if (!knownProps.includes(key)) {
|
|
81
276
|
runtimeError([
|
|
@@ -86,18 +281,16 @@ async function transpile(options = {}) {
|
|
|
86
281
|
if (!src) {
|
|
87
282
|
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for transpilation.$>{line}`]);
|
|
88
283
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
return await transpiler({ ast, format, mapperFile, includeDocument });
|
|
284
|
+
|
|
285
|
+
const sm = new SomMark({ src, format, mapperFile, includeDocument, plugins, excludePlugins, priority });
|
|
286
|
+
return await sm.transpile();
|
|
94
287
|
}
|
|
95
288
|
|
|
96
289
|
export {
|
|
97
290
|
HTML,
|
|
98
291
|
MARKDOWN,
|
|
99
|
-
|
|
100
|
-
|
|
292
|
+
MDX,
|
|
293
|
+
Json,
|
|
101
294
|
Mapper,
|
|
102
295
|
TagBuilder,
|
|
103
296
|
MarkdownBuilder,
|
|
@@ -106,6 +299,12 @@ export {
|
|
|
106
299
|
parse,
|
|
107
300
|
transpile,
|
|
108
301
|
TOKEN_TYPES,
|
|
109
|
-
labels
|
|
302
|
+
labels,
|
|
303
|
+
enableColor,
|
|
304
|
+
htmlTable,
|
|
305
|
+
list,
|
|
306
|
+
parseList,
|
|
307
|
+
safeArg,
|
|
308
|
+
todo
|
|
110
309
|
};
|
|
111
310
|
export default SomMark;
|
|
@@ -1,151 +1,246 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { HTML_TAGS } from "../../constants/html_tags.js";
|
|
3
|
+
import { HTML_PROPS } from "../../constants/html_props.js";
|
|
4
|
+
import { VOID_ELEMENTS } from "../../constants/void_elements.js";
|
|
5
|
+
import kebabize from "../../helpers/kebabize.js";
|
|
6
|
+
import { todo, list, htmlTable } from "../../helpers/utils.js";
|
|
7
|
+
|
|
8
|
+
class HtmlMapper extends Mapper {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
comment(text) {
|
|
13
|
+
return `<!-- ${text.replace(/^#/, "").trim()} -->`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
formatOutput(output, includeDocument) {
|
|
17
|
+
const todoRegex = /@@TODO_BLOCK:([\s\S]*?):([\s\S]*?)@@/g;
|
|
18
|
+
const statusMarkers = ["done", "x", "X", "-", ""];
|
|
19
|
+
output = output.replace(todoRegex, (match, body, arg0) => {
|
|
20
|
+
const bodyTrimmed = body.trim().toLowerCase();
|
|
21
|
+
const arg0Trimmed = arg0.trim().toLowerCase();
|
|
22
|
+
|
|
23
|
+
const bodyIsStatus = statusMarkers.includes(bodyTrimmed);
|
|
24
|
+
const arg0IsStatus = statusMarkers.includes(arg0Trimmed);
|
|
25
|
+
|
|
26
|
+
let finalStatus = arg0; // Default: arg is status
|
|
27
|
+
let finalTask = body; // Default: body is task
|
|
28
|
+
|
|
29
|
+
if (bodyIsStatus && !arg0IsStatus) {
|
|
30
|
+
finalStatus = body;
|
|
31
|
+
finalTask = arg0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const checked = todo(finalStatus);
|
|
35
|
+
return this.tag("div").body(this.tag("input").attributes({ type: "checkbox", disabled: true, checked }).selfClose() + " " + (finalTask || ""));
|
|
36
|
+
});
|
|
37
|
+
if (includeDocument) {
|
|
38
|
+
let finalHeader = this.header;
|
|
39
|
+
let styleContent = "";
|
|
40
|
+
const updateStyleTag = style => {
|
|
41
|
+
if (style) {
|
|
42
|
+
const styleTag = `<style>\n${style}\n</style>`;
|
|
43
|
+
if (!finalHeader.includes(styleTag)) {
|
|
44
|
+
finalHeader += styleTag + "\n";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
styleContent = this.styles.join("\n");
|
|
51
|
+
updateStyleTag(styleContent);
|
|
52
|
+
|
|
53
|
+
return `<!DOCTYPE html>\n<html>\n${finalHeader}\n<body>\n${output}\n</body>\n</html>\n`;
|
|
54
|
+
}
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const HTML = new HtmlMapper();
|
|
4
60
|
|
|
5
61
|
HTML.register(
|
|
6
|
-
|
|
7
|
-
({ args })
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
62
|
+
"Html",
|
|
63
|
+
function ({ args }) {
|
|
64
|
+
this.pageProps.pageTitle = this.safeArg(args, undefined, "title", null, null, this.pageProps.pageTitle);
|
|
65
|
+
this.pageProps.charset = this.safeArg(args, undefined, "charset", null, null, this.pageProps.charset);
|
|
66
|
+
this.pageProps.tabIcon.src = this.safeArg(args, undefined, "iconSrc", null, null, this.pageProps.tabIcon.src);
|
|
67
|
+
this.pageProps.tabIcon.type = this.safeArg(args, undefined, "iconType", null, null, this.pageProps.tabIcon.type);
|
|
68
|
+
this.pageProps.httpEquiv["X-UA-Compatible"] = this.safeArg(
|
|
13
69
|
args,
|
|
14
70
|
undefined,
|
|
15
71
|
"httpEquiv",
|
|
16
72
|
null,
|
|
17
73
|
null,
|
|
18
|
-
|
|
74
|
+
this.pageProps.httpEquiv["X-UA-Compatible"]
|
|
19
75
|
);
|
|
20
|
-
|
|
76
|
+
this.pageProps.viewport = this.safeArg(args, undefined, "viewport", null, null, this.pageProps.viewport);
|
|
77
|
+
|
|
78
|
+
// Global CSS Variables
|
|
79
|
+
let cssVars = "";
|
|
80
|
+
Object.keys(args).forEach(key => {
|
|
81
|
+
if (key.startsWith("--")) {
|
|
82
|
+
cssVars += `${key}:${args[key]};`;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (cssVars) {
|
|
87
|
+
this.addStyle(`:root { ${cssVars} }`);
|
|
88
|
+
}
|
|
89
|
+
|
|
21
90
|
return "";
|
|
22
91
|
},
|
|
23
92
|
{
|
|
24
|
-
|
|
25
|
-
type: "Block"
|
|
26
|
-
}
|
|
93
|
+
type: "Block"
|
|
27
94
|
}
|
|
28
95
|
);
|
|
29
96
|
|
|
30
97
|
// Block
|
|
31
98
|
HTML.register(
|
|
32
|
-
|
|
33
|
-
({ content })
|
|
99
|
+
"Block",
|
|
100
|
+
function ({ content }) {
|
|
34
101
|
return content;
|
|
35
102
|
},
|
|
36
103
|
{
|
|
37
|
-
|
|
38
|
-
type: "Block"
|
|
39
|
-
}
|
|
104
|
+
type: "Block"
|
|
40
105
|
}
|
|
41
106
|
);
|
|
42
|
-
//
|
|
43
|
-
HTML.register(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return tag("section").body(content);
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
rules: {
|
|
50
|
-
type: "Block"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
// Headings
|
|
55
|
-
["h1", "h2", "h3", "h4", "h5", "h6"].forEach(heading => {
|
|
56
|
-
HTML.register(heading, ({ content }) => {
|
|
57
|
-
return tag(heading).body(content);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
107
|
+
// Quote
|
|
108
|
+
HTML.register(["quote", "blockquote"], function ({ content }) {
|
|
109
|
+
return this.tag("blockquote").body(content);
|
|
110
|
+
}, { type: "Block" });
|
|
60
111
|
// Bold
|
|
61
|
-
HTML.register(
|
|
62
|
-
return tag("strong").body(content);
|
|
63
|
-
});
|
|
112
|
+
HTML.register("bold", function ({ content }) {
|
|
113
|
+
return this.tag("strong").body(content);
|
|
114
|
+
}, { type: "any" });
|
|
115
|
+
// Strike
|
|
116
|
+
HTML.register("strike", function ({ content }) {
|
|
117
|
+
return this.tag("s").body(content);
|
|
118
|
+
}, { type: "any" });
|
|
64
119
|
// Italic
|
|
65
|
-
HTML.register(
|
|
66
|
-
return tag("i").body(content);
|
|
67
|
-
});
|
|
120
|
+
HTML.register("italic", function ({ content }) {
|
|
121
|
+
return this.tag("i").body(content);
|
|
122
|
+
}, { type: "any" });
|
|
68
123
|
// Emphasis
|
|
69
|
-
HTML.register(
|
|
70
|
-
return tag("span").attributes({ style: "font-weight:bold; font-style: italic;" }).body(content);
|
|
71
|
-
});
|
|
124
|
+
HTML.register("emphasis", function ({ content }) {
|
|
125
|
+
return this.tag("span").attributes({ style: "font-weight:bold; font-style: italic;" }).body(content);
|
|
126
|
+
}, { type: "any" });
|
|
72
127
|
// Colored Text
|
|
73
|
-
HTML.register(
|
|
74
|
-
const color = safeArg(args, 0, undefined, null, null, "none");
|
|
75
|
-
return tag("span")
|
|
128
|
+
HTML.register("color", function ({ args, content }) {
|
|
129
|
+
const color = this.safeArg(args, 0, undefined, null, null, "none");
|
|
130
|
+
return this.tag("span")
|
|
76
131
|
.attributes({ style: `color:${color}` })
|
|
77
132
|
.body(content);
|
|
78
|
-
});
|
|
79
|
-
// Link
|
|
80
|
-
HTML.register(["link", "Link"], ({ args, content }) => {
|
|
81
|
-
const url = safeArg(args, 0, "url", null, null, "");
|
|
82
|
-
const title = safeArg(args, 1, "title", null, null, "");
|
|
83
|
-
return tag("a").attributes({ href: url.trim(), title: title.trim(), target: "_blank" }).body(content);
|
|
84
|
-
});
|
|
85
|
-
// Image
|
|
86
|
-
HTML.register(
|
|
87
|
-
["image", "Image"],
|
|
88
|
-
({ args }) => {
|
|
89
|
-
const src = safeArg(args, undefined, "src", null, null, "");
|
|
90
|
-
const alt = safeArg(args, undefined, "alt", null, null, "");
|
|
91
|
-
const width = safeArg(args, undefined, "width", null, null, "");
|
|
92
|
-
const height = safeArg(args, undefined, "height", null, null, "");
|
|
93
|
-
return tag("img").attributes({ src, alt, width, height }).selfClose();
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
rules: {
|
|
97
|
-
args: {
|
|
98
|
-
required: ["src"]
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
);
|
|
133
|
+
}, { type: "any" });
|
|
103
134
|
// Code
|
|
104
135
|
HTML.register(
|
|
105
|
-
|
|
106
|
-
({ args, content })
|
|
107
|
-
|
|
136
|
+
"Code",
|
|
137
|
+
function ({ args, content }) {
|
|
138
|
+
const lang = this.safeArg(args, 0, "lang", null, null, "text");
|
|
139
|
+
const code = content || "";
|
|
140
|
+
const code_element = this.tag("code");
|
|
141
|
+
|
|
142
|
+
code_element.attributes({ class: `language-${lang}` });
|
|
143
|
+
|
|
144
|
+
return this.tag("pre").body(code_element.body(code));
|
|
108
145
|
},
|
|
109
|
-
{ escape: false,
|
|
146
|
+
{ escape: false, type: "AtBlock" }
|
|
110
147
|
);
|
|
111
148
|
// List
|
|
112
149
|
HTML.register(
|
|
113
|
-
|
|
114
|
-
({ content })
|
|
115
|
-
return list(content);
|
|
150
|
+
"list",
|
|
151
|
+
function ({ content }) {
|
|
152
|
+
return list(content, "ul", this.escapeHTML);
|
|
116
153
|
},
|
|
117
|
-
{ escape: false }
|
|
154
|
+
{ escape: false, type: "any" }
|
|
118
155
|
);
|
|
119
|
-
// Table
|
|
120
156
|
HTML.register(
|
|
121
|
-
|
|
122
|
-
({ content, args })
|
|
123
|
-
return
|
|
157
|
+
"Table",
|
|
158
|
+
function ({ content, args }) {
|
|
159
|
+
return htmlTable(content.split(/\n/), args, this.escapeHTML);
|
|
124
160
|
},
|
|
125
161
|
{
|
|
126
162
|
escape: false,
|
|
127
|
-
|
|
128
|
-
type: "AtBlock"
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
// Horizontal Rule
|
|
133
|
-
HTML.register(
|
|
134
|
-
"hr",
|
|
135
|
-
() => {
|
|
136
|
-
return tag("hr").selfClose();
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
rules: {
|
|
140
|
-
is_Self_closing: true
|
|
141
|
-
}
|
|
163
|
+
type: "AtBlock"
|
|
142
164
|
}
|
|
143
165
|
);
|
|
166
|
+
|
|
144
167
|
// Todo
|
|
145
|
-
HTML.register("todo", ({ args, content })
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
HTML.register("todo", function ({ args, content }) {
|
|
169
|
+
const isPlaceholder = content.includes("__SOMMARK_BODY_PLACEHOLDER_");
|
|
170
|
+
if (isPlaceholder) {
|
|
171
|
+
return `@@TODO_BLOCK:${content}:${args[0] || ""}@@`;
|
|
172
|
+
}
|
|
173
|
+
const statusMarkers = ["done", "x", "X", "-", ""];
|
|
174
|
+
const isInline = !isPlaceholder && statusMarkers.includes(content.trim().toLowerCase()) && args.length > 0;
|
|
175
|
+
const status = isInline ? content : (args[0] || "");
|
|
176
|
+
const label = isInline ? (args[0] || "") : content;
|
|
177
|
+
const checked = todo(status);
|
|
178
|
+
return this.tag("div").body(this.tag("input").attributes({ type: "checkbox", disabled: true, checked }).selfClose() + " " + (label || ""));
|
|
179
|
+
}, { type: "any" });
|
|
180
|
+
|
|
181
|
+
HTML_TAGS.forEach(tagName => {
|
|
182
|
+
const idsToRegister = [tagName].filter(id => {
|
|
183
|
+
const existing = HTML.get(id);
|
|
184
|
+
if (!existing || !existing.id) return true;
|
|
185
|
+
return Array.isArray(existing.id) ? !existing.id.includes(id) : existing.id !== id;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
idsToRegister.forEach(id => {
|
|
189
|
+
const isAtBlock = ["style", "script"].includes(id.toLowerCase());
|
|
190
|
+
|
|
191
|
+
HTML.register(
|
|
192
|
+
id,
|
|
193
|
+
function ({ args, content }) {
|
|
194
|
+
const element = this.tag(id);
|
|
195
|
+
let inline_style = args.style ? (args.style.endsWith(";") ? args.style : args.style + ";") : "";
|
|
196
|
+
|
|
197
|
+
// Auto-ID for Headings
|
|
198
|
+
if (/^h[1-6]$/i.test(id) && !args.id && content) {
|
|
199
|
+
const idAttr = content
|
|
200
|
+
.toString()
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.replace(/[^\w\s-]/g, "")
|
|
203
|
+
.replace(/\s+/g, "-");
|
|
204
|
+
element.attributes({ id: idAttr });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const keys = Object.keys(args).filter(arg => isNaN(arg));
|
|
208
|
+
keys.forEach(key => {
|
|
209
|
+
if (key === "style") return; // Already handled
|
|
210
|
+
|
|
211
|
+
const isDimensionAttributeSupported = ["img", "video", "svg", "canvas", "iframe", "object", "embed"].includes(id.toLowerCase());
|
|
212
|
+
const isWidthOrHeight = key === "width" || key === "height";
|
|
213
|
+
const isEvent = key.toLowerCase().startsWith("on");
|
|
214
|
+
|
|
215
|
+
const k = isEvent ? key.toLowerCase() : (HTML_PROPS.has(key) || this.extraProps.has(key)) ? key : kebabize(key);
|
|
216
|
+
|
|
217
|
+
if (isEvent || ((HTML_PROPS.has(key) || this.extraProps.has(key)) && (!isWidthOrHeight || isDimensionAttributeSupported)) || k.startsWith("data-") || k.startsWith("aria-")) {
|
|
218
|
+
element.attributes({ [k]: args[key] });
|
|
219
|
+
} else {
|
|
220
|
+
inline_style += `${k}:${args[key]};`;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (inline_style) {
|
|
225
|
+
element.attributes({ style: inline_style });
|
|
226
|
+
}
|
|
227
|
+
// Self-Closing Element
|
|
228
|
+
if (VOID_ELEMENTS.has(id.toLowerCase())) {
|
|
229
|
+
return element.selfClose();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return element.body(content);
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: isAtBlock ? "AtBlock" : "Block",
|
|
236
|
+
escape: !isAtBlock,
|
|
237
|
+
rules: {
|
|
238
|
+
is_self_closing: VOID_ELEMENTS.has(id.toLowerCase())
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
});
|
|
149
243
|
});
|
|
150
244
|
|
|
245
|
+
|
|
151
246
|
export default HTML;
|