sommark 4.0.3 → 4.2.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 (43) hide show
  1. package/README.md +304 -73
  2. package/cli/cli.mjs +1 -1
  3. package/cli/commands/build.js +3 -1
  4. package/cli/commands/help.js +2 -0
  5. package/cli/commands/init.js +25 -6
  6. package/cli/constants.js +2 -1
  7. package/cli/helpers/transpile.js +5 -2
  8. package/constants/html_props.js +1 -0
  9. package/core/evaluator.js +1061 -0
  10. package/core/formats.js +15 -7
  11. package/core/helpers/config-loader.js +16 -8
  12. package/core/helpers/lib.js +72 -0
  13. package/core/helpers/preprocessor.js +202 -0
  14. package/core/helpers/runtimeOutput.js +28 -0
  15. package/core/helpers/url.js +12 -0
  16. package/core/labels.js +9 -2
  17. package/core/lexer.js +228 -61
  18. package/core/modules.js +338 -60
  19. package/core/parser.js +275 -55
  20. package/core/tokenTypes.js +11 -0
  21. package/core/transpiler.js +352 -66
  22. package/core/validator.js +70 -7
  23. package/formatter/tag.js +31 -7
  24. package/grammar.ebnf +21 -10
  25. package/helpers/fetch-fs.js +37 -0
  26. package/helpers/safeDataParser.js +3 -3
  27. package/helpers/spinner.js +97 -0
  28. package/helpers/utils.js +46 -0
  29. package/helpers/virtual-fs.js +29 -0
  30. package/index.browser.js +87 -0
  31. package/index.js +23 -332
  32. package/index.shared.js +443 -0
  33. package/mappers/languages/html.js +50 -9
  34. package/mappers/languages/json.js +81 -38
  35. package/mappers/languages/jsonc.js +82 -0
  36. package/mappers/languages/markdown.js +88 -48
  37. package/mappers/languages/mdx.js +50 -15
  38. package/mappers/languages/text.js +67 -0
  39. package/mappers/languages/xml.js +6 -6
  40. package/mappers/mapper.js +36 -4
  41. package/mappers/shared/index.js +12 -13
  42. package/package.json +11 -2
  43. package/core/formatter.js +0 -215
package/core/formats.js CHANGED
@@ -1,14 +1,22 @@
1
1
  /**
2
2
  * A list of supported output formats for SomMark.
3
3
  */
4
+ export const textFormat = "text";
5
+ export const htmlFormat = "html";
6
+ export const markdownFormat = "markdown";
7
+ export const mdxFormat = "mdx";
8
+ export const jsonFormat = "json";
9
+ export const jsoncFormat = "jsonc";
10
+ export const xmlFormat = "xml";
11
+
4
12
  const formats = {
5
- textFormat: "text",
6
- htmlFormat: "html",
7
- markdownFormat: "markdown",
8
- mdxFormat: "mdx",
9
- jsonFormat: "json",
10
- xmlFormat: "xml"
13
+ textFormat,
14
+ htmlFormat,
15
+ markdownFormat,
16
+ mdxFormat,
17
+ jsonFormat,
18
+ jsoncFormat,
19
+ xmlFormat
11
20
  };
12
21
 
13
- export const { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, xmlFormat } = formats;
14
22
  export default formats;
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path from "pathe";
2
2
  import fs from "node:fs/promises";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
@@ -40,7 +40,7 @@ export async function findAndLoadConfig(targetPath) {
40
40
  try {
41
41
  const absoluteTarget = path.resolve(cwd, targetPath);
42
42
  const stats = await fs.stat(absoluteTarget);
43
-
43
+
44
44
  // If target is a .js file, it might be an explicit config (legacy/internal support)
45
45
  if (stats.isFile() && absoluteTarget.endsWith(".js") && !absoluteTarget.endsWith("smark.config.js")) {
46
46
  configPath = absoluteTarget;
@@ -74,7 +74,7 @@ export async function findAndLoadConfig(targetPath) {
74
74
  // No config found in CWD
75
75
  }
76
76
  }
77
-
77
+
78
78
  const defaultConfig = {
79
79
  outputFile: "output",
80
80
  outputDir: startDir,
@@ -82,6 +82,14 @@ export async function findAndLoadConfig(targetPath) {
82
82
  removeComments: true,
83
83
  placeholders: {},
84
84
  customProps: [],
85
+ importAliases: {},
86
+ fallbackTarget: "style",
87
+ outputValidator: null,
88
+ baseDir: null,
89
+ showSpinner: false,
90
+ generateRuntimeOutput: false,
91
+ hideRuntimeOutput: false,
92
+ security: {},
85
93
  };
86
94
 
87
95
  if (configPath) {
@@ -89,12 +97,12 @@ export async function findAndLoadConfig(targetPath) {
89
97
  if (loadedConfig) {
90
98
  // Support both mapperFile and mappingFile (backwards compatibility)
91
99
  const finalMapper = loadedConfig.mapperFile || loadedConfig.mappingFile || defaultConfig.mapperFile;
92
-
93
- const finalConfig = {
94
- ...defaultConfig,
95
- ...loadedConfig,
100
+
101
+ const finalConfig = {
102
+ ...defaultConfig,
103
+ ...loadedConfig,
96
104
  mapperFile: finalMapper,
97
- resolvedConfigPath: configPath
105
+ resolvedConfigPath: configPath
98
106
  };
99
107
  if (loadedConfig.outputDir) {
100
108
  const configDir = path.dirname(configPath);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * SomMark Evaluator APIs
3
+ *
4
+ * Provides built-in utility methods safely bound to the sandbox VM.
5
+ */
6
+
7
+ // Host-defined compile that will be injected securely
8
+ let hostCompile = null;
9
+
10
+ export function registerHostCompile(fn) {
11
+ hostCompile = fn;
12
+ }
13
+
14
+ // Host-defined settings that will be injected securely
15
+ let hostSettings = {};
16
+
17
+ export function registerHostSettings(settings) {
18
+ hostSettings = settings || {};
19
+ }
20
+
21
+ const version = "4.2.0";
22
+
23
+ const SomMark = {
24
+ version,
25
+
26
+ get settings() {
27
+ return hostSettings;
28
+ },
29
+
30
+ // Secure recursive compile implementation
31
+ compile: async (src, options = {}) => {
32
+ if (!hostCompile) {
33
+ throw new Error("Compilation capability is not initialized.");
34
+ }
35
+ return hostCompile(src, options);
36
+ },
37
+
38
+ // Wrap string as safe raw HTML to skip automatic escaping
39
+ raw: (html) => {
40
+ return { __raw: String(html) };
41
+ },
42
+
43
+ // Register custom tag handlers programmatically within sandboxed environments
44
+ register: (id, render, options = {}) => {
45
+ throw new Error("SomMark.register can only be invoked within the sandboxed template logic environment.");
46
+ },
47
+
48
+ // Retrieve active tags by ID
49
+ get: (id) => {
50
+ throw new Error("SomMark.get can only be invoked within the sandboxed template logic environment.");
51
+ },
52
+
53
+ // Remove registered output handlers
54
+ removeOutput: (id) => {
55
+ throw new Error("SomMark.removeOutput can only be invoked within the sandboxed template logic environment.");
56
+ },
57
+
58
+ // Check if tag IDs are registered
59
+ includesId: (ids) => {
60
+ throw new Error("SomMark.includesId can only be invoked within the sandboxed template logic environment.");
61
+ },
62
+
63
+ // Programmatic HTML/XML tag generation utility
64
+ tag: (tagName) => {
65
+ throw new Error("SomMark.tag can only be invoked within the sandboxed template logic environment.");
66
+ }
67
+ };
68
+
69
+ // Freeze the entire Standard Library to make it completely immutable and tamper-proof
70
+ Object.freeze(SomMark);
71
+
72
+ export default SomMark;
@@ -0,0 +1,202 @@
1
+ import path from "pathe";
2
+ import * as acorn from "acorn";
3
+ import evaluator from "../evaluator.js";
4
+ import { transpilerError } from "../errors.js";
5
+
6
+ let _nodeFsCache;
7
+ async function getNodeFs() {
8
+ if (_nodeFsCache !== undefined) return _nodeFsCache;
9
+ try {
10
+ const m = await import("node:fs");
11
+ const raw = m.default || m;
12
+ _nodeFsCache = {
13
+ exists: (p) => raw.promises.access(p).then(() => true).catch(() => false),
14
+ readFile: (p, enc) => raw.promises.readFile(p, enc),
15
+ };
16
+ } catch {
17
+ _nodeFsCache = null;
18
+ }
19
+ return _nodeFsCache;
20
+ }
21
+
22
+ /**
23
+ * Preprocesses a runtime JS block, parsing it with Acorn to locate
24
+ * SomMark.static("...") and SomMark.import("...") calls, evaluating/loading them,
25
+ * and replacing them inline with LSP/runtime safety.
26
+ *
27
+ * @param {string} code - The raw javascript runtime code.
28
+ * @param {string|null} filename - Active template filename for relative imports.
29
+ * @param {Object} security - Security restrictions from the engine configuration.
30
+ * @returns {Promise<string>} - The preprocessed code.
31
+ */
32
+ export async function preprocessRuntimeLogic(code, filename = null, security = {}, instance = null) {
33
+ let ast;
34
+ try {
35
+ ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module" });
36
+ } catch (err) {
37
+ // Fallback: If code is not a fully valid JS block (e.g. an expression fragment),
38
+ // let it pass through to the standard renderer untouched.
39
+ return code;
40
+ }
41
+
42
+ const matches = [];
43
+
44
+ // Recursive AST Traversal to find SomMark.static and SomMark.import calls
45
+ function traverse(node) {
46
+ if (!node || typeof node !== "object") return;
47
+
48
+ if (
49
+ node.type === "CallExpression" &&
50
+ node.callee.type === "MemberExpression" &&
51
+ node.callee.object.name === "SomMark" &&
52
+ node.arguments.length > 0
53
+ ) {
54
+ const propName = node.callee.property.name;
55
+ if (propName === "static" || propName === "import") {
56
+ matches.push(node);
57
+ }
58
+ }
59
+
60
+ for (const key of Object.keys(node)) {
61
+ const child = node[key];
62
+ if (Array.isArray(child)) {
63
+ for (const item of child) traverse(item);
64
+ } else {
65
+ traverse(child);
66
+ }
67
+ }
68
+ }
69
+
70
+ traverse(ast);
71
+
72
+ let preprocessedCode = code;
73
+
74
+ if (matches.length > 0) {
75
+ // Sort matches right-to-left to prevent character offset drifting
76
+ matches.sort((a, b) => b.start - a.start);
77
+
78
+ // Execute/Import and replace inline
79
+ for (const match of matches) {
80
+ const propName = match.callee.property.name;
81
+ const argNode = match.arguments[0];
82
+ let argValue = "";
83
+
84
+ if (argNode.type === "Literal") {
85
+ argValue = String(argNode.value);
86
+ } else if (argNode.type === "TemplateLiteral") {
87
+ argValue = argNode.quasis.map((q) => q.value.cooked).join("");
88
+ }
89
+
90
+ if (propName === "static") {
91
+ if (!argValue) {
92
+ transpilerError([
93
+ `<$red:SomMark.static Argument Error:$> The argument to SomMark.static must be a string.{line}`
94
+ ]);
95
+ }
96
+
97
+ // If the code contains a top-level return statement, wrap it in an async IIFE
98
+ let finalStaticCode = argValue.trim();
99
+ if (finalStaticCode.includes("return")) {
100
+ finalStaticCode = `(async () => {\n${argValue}\n})()`;
101
+ }
102
+
103
+ // Run securely inside the active QuickJS VM sandbox
104
+ let result;
105
+ try {
106
+ result = await evaluator.execute(finalStaticCode);
107
+ } catch (err) {
108
+ transpilerError([
109
+ `<$red:SomMark.static Execution Error:$> ${err.message}{line}`,
110
+ `<$yellow:Static Code:$> <$blue:${argValue}$>{line}`
111
+ ]);
112
+ }
113
+
114
+ // Serialize the return value safely
115
+ let serialized = "";
116
+ if (result === undefined) {
117
+ serialized = "undefined";
118
+ } else if (typeof result === "object") {
119
+ serialized = JSON.stringify(result);
120
+ } else if (typeof result === "string") {
121
+ serialized = JSON.stringify(result); // Automatically escapes quotes and special chars
122
+ } else {
123
+ serialized = String(result);
124
+ }
125
+
126
+ // Slice out SomMark.static(...) and splice in the serialized value
127
+ preprocessedCode =
128
+ preprocessedCode.slice(0, match.start) +
129
+ serialized +
130
+ preprocessedCode.slice(match.end);
131
+ } else if (propName === "import") {
132
+ if (!argValue) {
133
+ transpilerError([
134
+ `<$red:SomMark.import Argument Error:$> The argument to SomMark.import must be a static, non-empty string literal.{line}`
135
+ ]);
136
+ }
137
+
138
+ // Resolve the file path relative to the template's base directory
139
+ let baseDir = instance?.cwd || "/";
140
+ if (filename && filename !== "anonymous") {
141
+ baseDir = path.dirname(path.resolve(filename));
142
+ }
143
+ const resolvedPath = path.resolve(baseDir, argValue);
144
+
145
+ const fsImpl = instance?.fs || await getNodeFs();
146
+
147
+ // File presence validation
148
+ if (!fsImpl || !await fsImpl.exists(resolvedPath)) {
149
+ transpilerError([
150
+ `<$red:SomMark.import File Error:$> File not found: <$magenta:${argValue}$>{line}`,
151
+ `<$yellow:Resolved Path:$> <$blue:${resolvedPath}$>{line}`
152
+ ]);
153
+ }
154
+
155
+ // Security Extension restriction validation
156
+ const ext = path.extname(resolvedPath).toLowerCase();
157
+ if (security?.allowedExtensions && !security.allowedExtensions.includes(ext)) {
158
+ transpilerError([
159
+ `<$red:Security Error:$> File extension <$yellow:${ext}$> is not allowed by security policy.{line}`,
160
+ `<$yellow:Import path:$> <$blue:${argValue}$>{line}`
161
+ ]);
162
+ }
163
+
164
+ let serialized = "";
165
+ const content = await fsImpl.readFile(resolvedPath, "utf-8");
166
+
167
+ if (ext === ".json") {
168
+ // Validate JSON structure
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(content);
172
+ } catch (err) {
173
+ transpilerError([
174
+ `<$red:JSON Parse Error:$> Failed to parse JSON file <$magenta:${argValue}$>:{line}`,
175
+ `<$yellow:Error:$> ${err.message}{line}`
176
+ ]);
177
+ }
178
+ serialized = JSON.stringify(parsed);
179
+ } else {
180
+ // Fallback for plain text, .smark, and other extensions: Serialize as JSON-escaped string
181
+ serialized = JSON.stringify(content);
182
+ }
183
+
184
+ // Slice out SomMark.import(...) and splice in the serialized content
185
+ preprocessedCode =
186
+ preprocessedCode.slice(0, match.start) +
187
+ serialized +
188
+ preprocessedCode.slice(match.end);
189
+ }
190
+ }
191
+ }
192
+
193
+ // LSP / Linter Safety Guard Injection
194
+ // If the resulting code references the global SomMark keyword, prepend fallback declarations to prevent crashes.
195
+ const hasSomMarkRef = /\bSomMark\b/.test(preprocessedCode);
196
+ if (hasSomMarkRef) {
197
+ const safetyGuard = `/* global SomMark */\nif (typeof globalThis.SomMark === 'undefined') { globalThis.SomMark = { static: (c) => c, import: (c) => c }; }\n`;
198
+ preprocessedCode = safetyGuard + preprocessedCode;
199
+ }
200
+
201
+ return preprocessedCode;
202
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Wraps runtime JavaScript logic inside target-specific scoped structures.
3
+ *
4
+ * @param {string} code - The preprocessed javascript code.
5
+ * @param {string} format - The active compilation target format (e.g., 'html', 'mdx').
6
+ * @param {string|null} parentId - The unique tracking identifier of the parent element.
7
+ * @param {boolean} isGlobal - Whether the script is at the root level or block-level.
8
+ * @returns {string} - The formatted, target-scoped runtime code.
9
+ */
10
+ export function wrapRuntimeLogic(code, format, parentId, isGlobal) {
11
+ const trimmedCode = code
12
+ .split("\n")
13
+ .filter(line => line.trim() !== "")
14
+ .join("\n");
15
+
16
+ if (isGlobal || !parentId) {
17
+ return `\n${trimmedCode}\n`;
18
+ }
19
+
20
+ const lowerFormat = format?.toLowerCase();
21
+ if (lowerFormat === "html" || lowerFormat === "mdx") {
22
+ const selfDefinition = `const self = document.querySelector('[data-sommark-id="${parentId}"]');`;
23
+ return `\n(async function(){${selfDefinition}\nif (self) {\n${trimmedCode}\n}\n})();\n`;
24
+ }
25
+
26
+ // Fallback/Default for other formats: return the raw code untouched
27
+ return `\n${trimmedCode}\n`;
28
+ }
@@ -0,0 +1,12 @@
1
+ export function fileURLToPath(url) {
2
+ if (typeof url !== "string") return url;
3
+ if (url.startsWith("file://")) {
4
+ let p = url.slice(7);
5
+ // On Windows (starts with /C:/ or similar), remove the leading slash:
6
+ if (/^\/[a-zA-Z]:/.test(p)) {
7
+ p = p.slice(1);
8
+ }
9
+ return p;
10
+ }
11
+ return url;
12
+ }
package/core/labels.js CHANGED
@@ -7,8 +7,13 @@ export const BLOCK = "Block",
7
7
  INLINE = "Inline",
8
8
  ATBLOCK = "AtBlock",
9
9
  COMMENT = "Comment",
10
+ COMMENT_BLOCK = "CommentBlock",
10
11
  IMPORT = "Import",
11
- USE_MODULE = "$use-module";
12
+ USE_MODULE = "$use-module",
13
+ SLOT = "Slot",
14
+ STATIC_LOGIC = "StaticLogic",
15
+ RUNTIME_LOGIC = "RuntimeLogic",
16
+ FOR_EACH = "ForEach";
12
17
 
13
18
  /**
14
19
  * Names for symbols used to separate parts of the code (like commas and colons).
@@ -36,4 +41,6 @@ export const block_id = "Block Identifier",
36
41
  atblock_key = "AtBlock Key",
37
42
  at_end = "Atblock End",
38
43
  /** Reserved keyword for closing blocks */
39
- end_keyword = "end";
44
+ end_keyword = "end",
45
+ slot_keyword = "slot",
46
+ for_each_keyword = "for-each";