sommark 4.0.2 → 4.1.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/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;
@@ -31,37 +31,42 @@ async function loadConfigFile(configPath) {
31
31
  * @returns {Promise<Object>} - The final configuration merged with defaults.
32
32
  */
33
33
  export async function findAndLoadConfig(targetPath) {
34
- let startDir = process.cwd();
34
+ const cwd = process.cwd();
35
35
  let configPath = null;
36
+ let startDir = cwd;
36
37
 
37
- // 1. Check if targetPath is an explicit config file path
38
+ // 1. Resolve Target Directory
38
39
  if (targetPath) {
39
40
  try {
40
- const stats = await fs.stat(targetPath);
41
- if (stats.isFile() && targetPath.endsWith(".js")) {
42
- configPath = path.resolve(targetPath);
41
+ const absoluteTarget = path.resolve(cwd, targetPath);
42
+ const stats = await fs.stat(absoluteTarget);
43
+
44
+ // If target is a .js file, it might be an explicit config (legacy/internal support)
45
+ if (stats.isFile() && absoluteTarget.endsWith(".js") && !absoluteTarget.endsWith("smark.config.js")) {
46
+ configPath = absoluteTarget;
43
47
  } else {
44
- startDir = stats.isDirectory() ? targetPath : path.dirname(targetPath);
48
+ startDir = stats.isDirectory() ? absoluteTarget : path.dirname(absoluteTarget);
45
49
  }
46
50
  } catch {
47
- // Path doesn't exist
51
+ // Path doesn't exist, fallback to CWD
48
52
  }
49
53
  }
50
54
 
51
- // 2. Check the target directory
55
+ // 2. Check the Target Directory (Highest Priority)
52
56
  if (!configPath) {
53
- const localConfig = path.join(startDir, CONFIG_FILE_NAME);
57
+ const targetConfig = path.join(startDir, CONFIG_FILE_NAME);
54
58
  try {
55
- await fs.access(localConfig);
56
- configPath = localConfig;
59
+ await fs.access(targetConfig);
60
+ configPath = targetConfig;
57
61
  } catch {
58
- // No local config found in target dir
62
+ // No config found in target dir
59
63
  }
60
64
  }
61
65
 
62
- // 3. Check the current working directory (if different from target dir)
63
- if (!configPath && startDir !== process.cwd()) {
64
- const cwdConfig = path.join(process.cwd(), CONFIG_FILE_NAME);
66
+ // 3. Check the Current Working Directory (Fallback)
67
+ // We only check CWD if it's different from the Target Directory
68
+ if (!configPath && startDir !== cwd) {
69
+ const cwdConfig = path.join(cwd, CONFIG_FILE_NAME);
65
70
  try {
66
71
  await fs.access(cwdConfig);
67
72
  configPath = cwdConfig;
@@ -77,6 +82,14 @@ export async function findAndLoadConfig(targetPath) {
77
82
  removeComments: true,
78
83
  placeholders: {},
79
84
  customProps: [],
85
+ importAliases: {},
86
+ fallbackTarget: "style",
87
+ outputValidator: null,
88
+ baseDir: null,
89
+ showSpinner: false,
90
+ generateRuntimeOutput: false,
91
+ hideRuntimeOutput: false,
92
+ security: {},
80
93
  };
81
94
 
82
95
  if (configPath) {
@@ -0,0 +1,75 @@
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 manualVersion = "4.1.0";
22
+ const version = await import(new URL("../../package.json", import.meta.url), { with: { type: "json" } })
23
+ .then(pkg => pkg.default.version || manualVersion)
24
+ .catch(() => manualVersion);
25
+
26
+ const SomMark = {
27
+ version,
28
+
29
+ get settings() {
30
+ return hostSettings;
31
+ },
32
+
33
+ // Secure recursive compile implementation
34
+ compile: async (src, options = {}) => {
35
+ if (!hostCompile) {
36
+ throw new Error("Compilation capability is not initialized.");
37
+ }
38
+ return hostCompile(src, options);
39
+ },
40
+
41
+ // Wrap string as safe raw HTML to skip automatic escaping
42
+ raw: (html) => {
43
+ return { __raw: String(html) };
44
+ },
45
+
46
+ // Register custom tag handlers programmatically within sandboxed environments
47
+ register: (id, render, options = {}) => {
48
+ throw new Error("SomMark.register can only be invoked within the sandboxed template logic environment.");
49
+ },
50
+
51
+ // Retrieve active tags by ID
52
+ get: (id) => {
53
+ throw new Error("SomMark.get can only be invoked within the sandboxed template logic environment.");
54
+ },
55
+
56
+ // Remove registered output handlers
57
+ removeOutput: (id) => {
58
+ throw new Error("SomMark.removeOutput can only be invoked within the sandboxed template logic environment.");
59
+ },
60
+
61
+ // Check if tag IDs are registered
62
+ includesId: (ids) => {
63
+ throw new Error("SomMark.includesId can only be invoked within the sandboxed template logic environment.");
64
+ },
65
+
66
+ // Programmatic HTML/XML tag generation utility
67
+ tag: (tagName) => {
68
+ throw new Error("SomMark.tag can only be invoked within the sandboxed template logic environment.");
69
+ }
70
+ };
71
+
72
+ // Freeze the entire Standard Library to make it completely immutable and tamper-proof
73
+ Object.freeze(SomMark);
74
+
75
+ export default SomMark;
@@ -0,0 +1,185 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import * as acorn from "acorn";
4
+ import evaluator from "../evaluator.js";
5
+ import { transpilerError } from "../errors.js";
6
+
7
+ /**
8
+ * Preprocesses a runtime JS block, parsing it with Acorn to locate
9
+ * SomMark.static("...") and SomMark.import("...") calls, evaluating/loading them,
10
+ * and replacing them inline with LSP/runtime safety.
11
+ *
12
+ * @param {string} code - The raw javascript runtime code.
13
+ * @param {string|null} filename - Active template filename for relative imports.
14
+ * @param {Object} security - Security restrictions from the engine configuration.
15
+ * @returns {Promise<string>} - The preprocessed code.
16
+ */
17
+ export async function preprocessRuntimeLogic(code, filename = null, security = {}) {
18
+ let ast;
19
+ try {
20
+ ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module" });
21
+ } catch (err) {
22
+ // Fallback: If code is not a fully valid JS block (e.g. an expression fragment),
23
+ // let it pass through to the standard renderer untouched.
24
+ return code;
25
+ }
26
+
27
+ const matches = [];
28
+
29
+ // Recursive AST Traversal to find SomMark.static and SomMark.import calls
30
+ function traverse(node) {
31
+ if (!node || typeof node !== "object") return;
32
+
33
+ if (
34
+ node.type === "CallExpression" &&
35
+ node.callee.type === "MemberExpression" &&
36
+ node.callee.object.name === "SomMark" &&
37
+ node.arguments.length > 0
38
+ ) {
39
+ const propName = node.callee.property.name;
40
+ if (propName === "static" || propName === "import") {
41
+ matches.push(node);
42
+ }
43
+ }
44
+
45
+ for (const key of Object.keys(node)) {
46
+ const child = node[key];
47
+ if (Array.isArray(child)) {
48
+ for (const item of child) traverse(item);
49
+ } else {
50
+ traverse(child);
51
+ }
52
+ }
53
+ }
54
+
55
+ traverse(ast);
56
+
57
+ let preprocessedCode = code;
58
+
59
+ if (matches.length > 0) {
60
+ // Sort matches right-to-left to prevent character offset drifting
61
+ matches.sort((a, b) => b.start - a.start);
62
+
63
+ // Execute/Import and replace inline
64
+ for (const match of matches) {
65
+ const propName = match.callee.property.name;
66
+ const argNode = match.arguments[0];
67
+ let argValue = "";
68
+
69
+ if (argNode.type === "Literal") {
70
+ argValue = String(argNode.value);
71
+ } else if (argNode.type === "TemplateLiteral") {
72
+ argValue = argNode.quasis.map((q) => q.value.cooked).join("");
73
+ }
74
+
75
+ if (propName === "static") {
76
+ if (!argValue) {
77
+ transpilerError([
78
+ `<$red:SomMark.static Argument Error:$> The argument to SomMark.static must be a string.{line}`
79
+ ]);
80
+ }
81
+
82
+ // If the code contains a top-level return statement, wrap it in an async IIFE
83
+ let finalStaticCode = argValue.trim();
84
+ if (finalStaticCode.includes("return")) {
85
+ finalStaticCode = `(async () => {\n${argValue}\n})()`;
86
+ }
87
+
88
+ // Run securely inside the active QuickJS VM sandbox
89
+ let result;
90
+ try {
91
+ result = await evaluator.execute(finalStaticCode);
92
+ } catch (err) {
93
+ transpilerError([
94
+ `<$red:SomMark.static Execution Error:$> ${err.message}{line}`,
95
+ `<$yellow:Static Code:$> <$blue:${argValue}$>{line}`
96
+ ]);
97
+ }
98
+
99
+ // Serialize the return value safely
100
+ let serialized = "";
101
+ if (result === undefined) {
102
+ serialized = "undefined";
103
+ } else if (typeof result === "object") {
104
+ serialized = JSON.stringify(result);
105
+ } else if (typeof result === "string") {
106
+ serialized = JSON.stringify(result); // Automatically escapes quotes and special chars
107
+ } else {
108
+ serialized = String(result);
109
+ }
110
+
111
+ // Slice out SomMark.static(...) and splice in the serialized value
112
+ preprocessedCode =
113
+ preprocessedCode.slice(0, match.start) +
114
+ serialized +
115
+ preprocessedCode.slice(match.end);
116
+ } else if (propName === "import") {
117
+ if (!argValue) {
118
+ transpilerError([
119
+ `<$red:SomMark.import Argument Error:$> The argument to SomMark.import must be a static, non-empty string literal.{line}`
120
+ ]);
121
+ }
122
+
123
+ // Resolve the file path relative to the template's base directory
124
+ let baseDir = process.cwd();
125
+ if (filename && filename !== "anonymous") {
126
+ baseDir = path.dirname(path.resolve(filename));
127
+ }
128
+ const resolvedPath = path.resolve(baseDir, argValue);
129
+
130
+ // File presence validation
131
+ if (!fs.existsSync(resolvedPath)) {
132
+ transpilerError([
133
+ `<$red:SomMark.import File Error:$> File not found: <$magenta:${argValue}$>{line}`,
134
+ `<$yellow:Resolved Path:$> <$blue:${resolvedPath}$>{line}`
135
+ ]);
136
+ }
137
+
138
+ // Security Extension restriction validation
139
+ const ext = path.extname(resolvedPath).toLowerCase();
140
+ if (security?.allowedExtensions && !security.allowedExtensions.includes(ext)) {
141
+ transpilerError([
142
+ `<$red:Security Error:$> File extension <$yellow:${ext}$> is not allowed by security policy.{line}`,
143
+ `<$yellow:Import path:$> <$blue:${argValue}$>{line}`
144
+ ]);
145
+ }
146
+
147
+ let serialized = "";
148
+ const content = fs.readFileSync(resolvedPath, "utf-8");
149
+
150
+ if (ext === ".json") {
151
+ // Validate JSON structure
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(content);
155
+ } catch (err) {
156
+ transpilerError([
157
+ `<$red:JSON Parse Error:$> Failed to parse JSON file <$magenta:${argValue}$>:{line}`,
158
+ `<$yellow:Error:$> ${err.message}{line}`
159
+ ]);
160
+ }
161
+ serialized = JSON.stringify(parsed);
162
+ } else {
163
+ // Fallback for plain text, .smark, and other extensions: Serialize as JSON-escaped string
164
+ serialized = JSON.stringify(content);
165
+ }
166
+
167
+ // Slice out SomMark.import(...) and splice in the serialized content
168
+ preprocessedCode =
169
+ preprocessedCode.slice(0, match.start) +
170
+ serialized +
171
+ preprocessedCode.slice(match.end);
172
+ }
173
+ }
174
+ }
175
+
176
+ // LSP / Linter Safety Guard Injection
177
+ // If the resulting code references the global SomMark keyword, prepend fallback declarations to prevent crashes.
178
+ const hasSomMarkRef = /\bSomMark\b/.test(preprocessedCode);
179
+ if (hasSomMarkRef) {
180
+ const safetyGuard = `/* global SomMark */\nif (typeof globalThis.SomMark === 'undefined') { globalThis.SomMark = { static: (c) => c, import: (c) => c }; }\n`;
181
+ preprocessedCode = safetyGuard + preprocessedCode;
182
+ }
183
+
184
+ return preprocessedCode;
185
+ }
@@ -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
+ }
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";