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.
Files changed (45) hide show
  1. package/README.md +47 -42
  2. package/SOMMARK-SPEC.md +483 -0
  3. package/cli/cli.mjs +42 -2
  4. package/cli/commands/color.js +36 -0
  5. package/cli/commands/help.js +7 -0
  6. package/cli/commands/init.js +2 -0
  7. package/cli/commands/list.js +119 -0
  8. package/cli/commands/print.js +61 -11
  9. package/cli/commands/show.js +24 -27
  10. package/cli/constants.js +1 -1
  11. package/cli/helpers/config.js +14 -4
  12. package/cli/helpers/transpile.js +27 -32
  13. package/constants/html_props.js +100 -0
  14. package/constants/html_tags.js +146 -0
  15. package/constants/void_elements.js +26 -0
  16. package/core/lexer.js +70 -39
  17. package/core/parser.js +100 -84
  18. package/core/pluginManager.js +139 -0
  19. package/core/plugins/comment-remover.js +47 -0
  20. package/core/plugins/module-system.js +137 -0
  21. package/core/plugins/quote-escaper.js +37 -0
  22. package/core/plugins/raw-content-plugin.js +72 -0
  23. package/core/plugins/rules-validation-plugin.js +197 -0
  24. package/core/plugins/sommark-format.js +211 -0
  25. package/core/transpiler.js +65 -198
  26. package/debug.js +9 -4
  27. package/format.js +23 -0
  28. package/formatter/mark.js +3 -3
  29. package/formatter/tag.js +6 -2
  30. package/grammar.ebnf +5 -5
  31. package/helpers/camelize.js +2 -0
  32. package/helpers/colorize.js +20 -14
  33. package/helpers/kebabize.js +2 -0
  34. package/helpers/utils.js +161 -0
  35. package/index.js +243 -44
  36. package/mappers/languages/html.js +200 -105
  37. package/mappers/languages/json.js +23 -4
  38. package/mappers/languages/markdown.js +88 -67
  39. package/mappers/languages/mdx.js +130 -2
  40. package/mappers/mapper.js +77 -246
  41. package/package.json +7 -5
  42. package/unformatted.smark +90 -0
  43. package/v3-todo.smark +75 -0
  44. package/CHANGELOG.md +0 -119
  45. 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
- const accepted_formats = [textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat];
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
- if (this.format && !accepted_formats.includes(this.format)) {
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
- this.mapperFile = mapperFiles[this.format];
141
+ runtimeError([`{line}<$red:Unknown Format$>: <$yellow:Mapper for format '${this.format}' not found.$>{line}`]);
36
142
  }
37
143
  }
38
- removeOutput = id => {
39
- this.mapperFile.outputs = this.mapperFile.outputs.filter(output => {
40
- if (Array.isArray(output.id)) {
41
- return !output.id.some(singleId => singleId === id);
42
- } else {
43
- return output.id !== id;
44
- }
45
- });
46
- };
47
- lex() {
48
- return lexer(this.src);
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
- parse() {
51
- const tokens = this.lex();
52
- return parser(tokens);
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
- async transpile() {
55
- const ast = this.parse();
56
- return await transpiler({ ast, format: this.format, mapperFile: this.mapperFile, includeDocument: this.includeDocument });
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 => lexer(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
- const tokens = lex(src);
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 = HTML, includeDocument = true } = options;
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
- const ast = parse(src);
90
- if (!Array.isArray(ast) || ast.length === 0) {
91
- runtimeError([`{line}<$red:Invalid AST:$> <$yellow:Transpiler expected a non-empty array AST.$>{line}`]);
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
- MDX,
100
- Json,
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
- const HTML = new Mapper();
3
- const { tag, code, list, safeArg } = HTML;
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
- ["Html", "html"],
7
- ({ args }) => {
8
- HTML.pageProps.pageTitle = safeArg(args, undefined, "title", null, null, HTML.pageProps.pageTitle);
9
- HTML.pageProps.charset = safeArg(args, undefined, "charset", null, null, HTML.pageProps.charset);
10
- HTML.pageProps.tabIcon.src = safeArg(args, undefined, "iconSrc", null, null, HTML.pageProps.tabIcon.src);
11
- HTML.pageProps.tabIcon.type = safeArg(args, undefined, "iconType", null, null, HTML.pageProps.tabIcon.type);
12
- HTML.pageProps.httpEquiv["X-UA-Compatible"] = safeArg(
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
- HTML.pageProps.httpEquiv["X-UA-Compatible"]
74
+ this.pageProps.httpEquiv["X-UA-Compatible"]
19
75
  );
20
- HTML.pageProps.viewport = safeArg(args, undefined, "viewport", null, null, HTML.pageProps.viewport);
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
- rules: {
25
- type: "Block"
26
- }
93
+ type: "Block"
27
94
  }
28
95
  );
29
96
 
30
97
  // Block
31
98
  HTML.register(
32
- ["Block", "block"],
33
- ({ content }) => {
99
+ "Block",
100
+ function ({ content }) {
34
101
  return content;
35
102
  },
36
103
  {
37
- rules: {
38
- type: "Block"
39
- }
104
+ type: "Block"
40
105
  }
41
106
  );
42
- // Section
43
- HTML.register(
44
- ["Section", "section"],
45
- ({ content }) => {
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(["bold", "Bold", "b"], ({ content }) => {
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(["italic", "i"], ({ content }) => {
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(["emphasis", "e"], ({ content }) => {
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(["color", "Color"], ({ args, content }) => {
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
- ["code", "Code"],
106
- ({ args, content }) => {
107
- return code(args, content);
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, rules: { type: "AtBlock" } }
146
+ { escape: false, type: "AtBlock" }
110
147
  );
111
148
  // List
112
149
  HTML.register(
113
- ["list", "List"],
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
- ["table", "Table"],
122
- ({ content, args }) => {
123
- return HTML.htmlTable(content.split(/\n/), args);
157
+ "Table",
158
+ function ({ content, args }) {
159
+ return htmlTable(content.split(/\n/), args, this.escapeHTML);
124
160
  },
125
161
  {
126
162
  escape: false,
127
- rules: {
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 checked = HTML.todo(content);
147
- const task = safeArg(args, 0, undefined, null, null, "");
148
- return tag("div").body(tag("input").attributes({ type: "checkbox", disabled: true, checked }).selfClose() + task);
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;