sommark 3.3.1 → 3.3.3

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/SOMMARK-SPEC.md CHANGED
@@ -50,8 +50,6 @@ Identifiers **must not contain spaces**.
50
50
 
51
51
  # 2. Blocks
52
52
 
53
- > At the top level, only Blocks and comments are permitted.
54
-
55
53
 
56
54
  Blocks are hierarchical containers that can contain other blocks, inline statements, or plain text.
57
55
 
@@ -39,7 +39,7 @@ export async function runBuild(format_option, sourcePath, outputFlag, outputFile
39
39
  if (await isExist(sourcePath)) {
40
40
  const file = path.parse(sourcePath);
41
41
  if (file.ext === ".smark") {
42
- const config = await loadConfig();
42
+ const config = await loadConfig(sourcePath);
43
43
 
44
44
  const success_msg = (outputDir, outputFile, size, date) => {
45
45
  return formatMessage(
@@ -20,6 +20,9 @@ export function getConfigDir() {
20
20
 
21
21
  export async function runInit() {
22
22
  try {
23
+ const configDir = getConfigDir();
24
+ const configFilePath = path.join(configDir, "smark.config.js");
25
+
23
26
  // ======================================================
24
27
  // Create configuration directory
25
28
  // ======================================================
@@ -33,7 +33,7 @@ export async function printLex(filePath) {
33
33
  const fileName = path.basename(filePath);
34
34
  console.log(formatMessage(`{line}<$blue: Printing tokens for$> <$yellow:'${fileName}'$>{line}`));
35
35
  const source_code = await readContent(filePath);
36
- const config = await loadConfig();
36
+ const config = await loadConfig(filePath);
37
37
 
38
38
  const absolutePath = path.resolve(process.cwd(), filePath);
39
39
  const smark = new SomMark({
@@ -59,7 +59,7 @@ export async function printParse(filePath) {
59
59
  const fileName = path.basename(filePath);
60
60
  console.log(formatMessage(`{line}<$blue: Printing AST for$> <$yellow:'${fileName}'$>{line}`));
61
61
  const source_code = await readContent(filePath);
62
- const config = await loadConfig();
62
+ const config = await loadConfig(filePath);
63
63
 
64
64
  const absolutePath = path.resolve(process.cwd(), filePath);
65
65
  const smark = new SomMark({
@@ -1,62 +1,17 @@
1
- import path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
- import { isExist } from "./file.js";
4
- import { getConfigDir } from "../commands/init.js";
1
+ import { findAndLoadConfig } from "../../core/helpers/config-loader.js";
5
2
 
6
3
  // ========================================================================== //
7
4
  // Configuration Loader //
8
5
  // ========================================================================== //
9
6
 
10
- const CONFIG_FILE_NAME = "smark.config.js";
11
- const currentDir = process.cwd();
12
- const localConfigPath = path.join(currentDir, CONFIG_FILE_NAME);
13
-
14
- // ========================================================================== //
15
- // Default Configuration //
16
- // ========================================================================== //
17
- let config = {
18
- outputFile: "output",
19
- outputDir: "",
20
- mappingFile: "",
21
- plugins: [],
22
- priority: []
23
- };
24
-
25
- // ========================================================================== //
26
- // Load Configuration //
27
- // ========================================================================== //
28
7
  let resolvedConfigPath = null;
29
8
 
30
- export async function loadConfig() {
31
- const userConfigPath = path.join(getConfigDir(), CONFIG_FILE_NAME);
32
- let targetConfigPath = null;
33
-
34
- if (await isExist(localConfigPath)) {
35
- targetConfigPath = localConfigPath;
36
- } else if (await isExist(userConfigPath)) {
37
- targetConfigPath = userConfigPath;
38
- }
39
-
40
- resolvedConfigPath = targetConfigPath;
41
-
42
- if (targetConfigPath) {
43
- try {
44
- const configURL = pathToFileURL(targetConfigPath).href;
45
- const loadedModule = await import(configURL);
46
- config = loadedModule.default || loadedModule;
47
- } catch (error) {
48
- console.error(`Error loading configuration file ${targetConfigPath}:`, error.message);
49
- }
50
- } else {
51
- // console.log(`${CONFIG_FILE_NAME} not found. Using default configuration.`);
52
- }
53
-
54
- if (!config.outputDir) {
55
- config.outputDir = process.cwd();
56
- }
57
- return config;
9
+ export async function loadConfig(filename = null) {
10
+ const config = await findAndLoadConfig(filename);
11
+ resolvedConfigPath = config.resolvedConfigPath;
12
+ return config;
58
13
  }
59
14
 
60
15
  export function getResolvedConfigPath() {
61
- return resolvedConfigPath;
16
+ return resolvedConfigPath;
62
17
  }
@@ -16,7 +16,7 @@ const default_mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [m
16
16
  // Transpile Function //
17
17
  // ========================================================================== //
18
18
  export async function transpile({ src, format, filename = null, mappingFile = "" }) {
19
- const config = await loadConfig();
19
+ const config = await loadConfig(filename);
20
20
  let finalMapper = mappingFile;
21
21
 
22
22
  // 1. Resolve Mapping File
package/core/errors.js CHANGED
@@ -38,6 +38,34 @@ function formatMessage(text) {
38
38
  return text;
39
39
  }
40
40
 
41
+ /**
42
+ * Formats an error with source context (line number, code snippet, pointer).
43
+ */
44
+ function formatErrorWithContext(src, range, filename, message, typeName) {
45
+ if (!src || !range || !range.start) return message;
46
+
47
+ const lines = src.split("\n");
48
+ const lineIndex = range.start.line;
49
+ const lineContent = lines[lineIndex] || "";
50
+ const pointerPadding = " ".repeat(range.start.character);
51
+ const sourceLabel = filename ? ` [${filename}]` : "";
52
+
53
+ const rangeInfo =
54
+ range.start.line === range.end.line
55
+ ? `from column <$yellow:${range.start.character}$> to <$yellow:${range.end.character}$>`
56
+ : `from line <$yellow:${range.start.line + 1}$>, column <$yellow:${range.start.character}$> to line <$yellow:${range.end.line + 1}$>, column <$yellow:${range.end.character}$>`;
57
+
58
+ const formattedMessage = [
59
+ `<$blue:{line}$><$red:Here where error occurred${sourceLabel}:$>{N}${lineContent}{N}${pointerPadding}<$yellow:^$>{N}{N}`,
60
+ `<$red:${typeName} Error:$> `,
61
+ ...(Array.isArray(message) ? message : [message]),
62
+ `{N}at line <$yellow:${range.start.line + 1}$>, ${rangeInfo}{N}`,
63
+ "<$blue:{line}$>"
64
+ ];
65
+
66
+ return formattedMessage;
67
+ }
68
+
41
69
  // ========================================================================== //
42
70
  // Error Classes //
43
71
  // ========================================================================== //
@@ -94,45 +122,39 @@ class SommarkError extends CustomError {
94
122
  // ========================================================================== //
95
123
 
96
124
  function getError(type) {
97
- const validate_msg = msg => Array.isArray(msg) && msg.length > 0;
98
- switch (type) {
99
- case "parser":
100
- return errorMessage => {
101
- if (validate_msg(errorMessage)) {
102
- throw new ParserError(errorMessage).message;
103
- }
104
- };
105
- case "transpiler":
106
- return errorMessage => {
107
- if (validate_msg(errorMessage)) {
108
- throw new TranspilerError(errorMessage).message;
109
- }
110
- };
111
- case "lexer":
112
- return errorMessage => {
113
- if (validate_msg(errorMessage)) {
114
- throw new LexerError(errorMessage).message;
115
- }
116
- };
117
- case "cli":
118
- return errorMessage => {
119
- if (validate_msg(errorMessage)) {
120
- throw new CLIError(errorMessage).message;
121
- }
122
- };
123
- case "runtime":
124
- return errorMessage => {
125
- if (validate_msg(errorMessage)) {
126
- throw new RuntimeError(errorMessage).message;
127
- }
128
- };
129
- case "sommark":
130
- return errorMessage => {
131
- if (validate_msg(errorMessage)) {
132
- throw new SommarkError(errorMessage).message;
133
- }
134
- };
135
- }
125
+ const validate_msg = msg => (Array.isArray(msg) && msg.length > 0) || typeof msg === "string";
126
+ const typeNames = {
127
+ parser: "Parser",
128
+ transpiler: "Transpiler",
129
+ lexer: "Lexer",
130
+ cli: "CLI",
131
+ runtime: "Runtime",
132
+ sommark: "SomMark"
133
+ };
134
+ const ErrorClasses = {
135
+ parser: ParserError,
136
+ transpiler: TranspilerError,
137
+ lexer: LexerError,
138
+ cli: CLIError,
139
+ runtime: RuntimeError,
140
+ sommark: SommarkError
141
+ };
142
+
143
+ return (errorMessage, context = null) => {
144
+ if (validate_msg(errorMessage)) {
145
+ let finalMessage = errorMessage;
146
+ if (context && context.src && context.range) {
147
+ finalMessage = formatErrorWithContext(
148
+ context.src,
149
+ context.range,
150
+ context.filename,
151
+ errorMessage,
152
+ typeNames[type]
153
+ );
154
+ }
155
+ throw new ErrorClasses[type](finalMessage).message;
156
+ }
157
+ };
136
158
  }
137
159
 
138
160
  const lexerError = getError("lexer");
@@ -0,0 +1,101 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import fs from "node:fs/promises";
4
+ import { pathToFileURL, fileURLToPath } from "node:url";
5
+
6
+ const CONFIG_FILE_NAME = "smark.config.js";
7
+
8
+ /**
9
+ * Gets the global configuration directory based on the OS.
10
+ */
11
+ export function getConfigDir() {
12
+ const homeDir = os.homedir();
13
+ if (process.platform === "win32") {
14
+ return path.join(process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), "sommark");
15
+ } else if (process.platform === "darwin") {
16
+ return path.join(homeDir, "Library", "Application Support", "sommark");
17
+ } else {
18
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(homeDir, ".config"), "sommark");
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Recursively searches for smark.config.js up the directory tree starting from startDir.
24
+ */
25
+ async function findConfig(startDir) {
26
+ let currentDir = startDir;
27
+ while (currentDir !== path.parse(currentDir).root) {
28
+ const configPath = path.join(currentDir, CONFIG_FILE_NAME);
29
+ try {
30
+ await fs.access(configPath);
31
+ return configPath;
32
+ } catch {
33
+ currentDir = path.dirname(currentDir);
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Loads the configuration from a file.
41
+ */
42
+ async function loadConfigFile(configPath) {
43
+ if (!configPath) return null;
44
+ try {
45
+ // Use a timestamp to bypass cache for dynamic updates in LSP
46
+ const configURL = `${pathToFileURL(configPath).href}?t=${Date.now()}`;
47
+ const loadedModule = await import(configURL);
48
+ return loadedModule.default || loadedModule;
49
+ } catch (error) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Finds and loads the configuration starting from targetPath.
56
+ * Checks local directory, parent directories, and finally the global config directory.
57
+ */
58
+ export async function findAndLoadConfig(targetPath) {
59
+ const startDir = targetPath ? (await fs.stat(targetPath)).isDirectory() ? targetPath : path.dirname(targetPath) : process.cwd();
60
+
61
+ let configPath = await findConfig(startDir);
62
+
63
+ if (!configPath) {
64
+ // As a fallback, check the current working directory of the process
65
+ // This helps LSP find the project config when running in the project root
66
+ const localConfigPath = path.join(process.cwd(), CONFIG_FILE_NAME);
67
+ try {
68
+ await fs.access(localConfigPath);
69
+ configPath = localConfigPath;
70
+ } catch {
71
+ // Not in CWD
72
+ }
73
+ }
74
+
75
+ if (!configPath) {
76
+ const globalConfigPath = path.join(getConfigDir(), CONFIG_FILE_NAME);
77
+ try {
78
+ await fs.access(globalConfigPath);
79
+ configPath = globalConfigPath;
80
+ } catch {
81
+ // No config found
82
+ }
83
+ }
84
+
85
+ const defaultConfig = {
86
+ outputFile: "output",
87
+ outputDir: startDir,
88
+ mappingFile: null,
89
+ plugins: [],
90
+ priority: []
91
+ };
92
+
93
+ if (configPath) {
94
+ const loadedConfig = await loadConfigFile(configPath);
95
+ if (loadedConfig) {
96
+ return { ...defaultConfig, ...loadedConfig, resolvedConfigPath: configPath };
97
+ }
98
+ }
99
+
100
+ return { ...defaultConfig, resolvedConfigPath: null };
101
+ }
@@ -10,7 +10,7 @@ const RulesValidationPlugin = {
10
10
  type: "on-ast",
11
11
  author: "Adam-Elmi",
12
12
  description: "Checks your document to make sure all tags and arguments follow the rules set in the mapper.",
13
- onAst(ast, { mapperFile }) {
13
+ onAst(ast, { mapperFile, instance }) {
14
14
  if (!mapperFile) return ast;
15
15
 
16
16
  const validateNode = (node, parentTarget = null) => {
@@ -20,7 +20,7 @@ const RulesValidationPlugin = {
20
20
  // 1. TEXT nodes validation //
21
21
  // ========================================================================== //
22
22
  if (node.type === "Text" && parentTarget) {
23
- this.runValidations(parentTarget, null, node.text, "Text", mapperFile);
23
+ this.runValidations(node, parentTarget, null, node.text, "Text", mapperFile, instance);
24
24
  }
25
25
 
26
26
  // ========================================================================== //
@@ -29,7 +29,7 @@ const RulesValidationPlugin = {
29
29
  if (node.id) {
30
30
  const target = mapperFile.get(node.id);
31
31
  if (target) {
32
- this.runValidations(target, node.args, this.getContent(node), node.type, mapperFile);
32
+ this.runValidations(node, target, node.args, this.getContent(node), node.type, mapperFile, instance);
33
33
  }
34
34
  }
35
35
 
@@ -54,10 +54,11 @@ const RulesValidationPlugin = {
54
54
  return "";
55
55
  },
56
56
 
57
- runValidations(target, args, content, type, mapperFile) {
57
+ runValidations(node, target, args, content, type, mapperFile, instance) {
58
58
  if (!target.options) return;
59
59
  const rules = target.options.rules || {};
60
60
  const id = Array.isArray(target.id) ? target.id.join(" | ") : target.id;
61
+ const context = instance ? { src: instance.src, range: node.range, filename: instance.filename } : null;
61
62
 
62
63
  // ========================================================================== //
63
64
  // 1. Validate Args Count & Keys //
@@ -68,24 +69,33 @@ const RulesValidationPlugin = {
68
69
  const argCount = args.length;
69
70
 
70
71
  if (min !== undefined && argCount < min) {
71
- transpilerError([
72
- "{line}<$red:Validation Error:$> ",
73
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:requires at least$> <$green:${min}$> <$yellow:argument(s). Found$> <$red:${argCount}$>{line}`
74
- ]);
72
+ transpilerError(
73
+ [
74
+ "<$red:Validation Error:$> ",
75
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:requires at least$> <$green:${min}$> <$yellow:argument(s). Found$> <$red:${argCount}$>`
76
+ ],
77
+ context
78
+ );
75
79
  }
76
80
  if (max !== undefined && argCount > max) {
77
- transpilerError([
78
- "{line}<$red:Validation Error:$> ",
79
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:accepts at most$> <$green:${max}$> <$yellow:argument(s). Found$> <$red:${argCount}$>{line}`
80
- ]);
81
+ transpilerError(
82
+ [
83
+ "<$red:Validation Error:$> ",
84
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:accepts at most$> <$green:${max}$> <$yellow:argument(s). Found$> <$red:${argCount}$>`
85
+ ],
86
+ context
87
+ );
81
88
  }
82
89
  if (required && Array.isArray(required)) {
83
90
  const missingKeys = required.filter(key => !Object.prototype.hasOwnProperty.call(args, key));
84
91
  if (missingKeys.length > 0) {
85
- transpilerError([
86
- "{line}<$red:Validation Error:$> ",
87
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required argument(s):$> <$red:${missingKeys.join(", ")}$>{line}`
88
- ]);
92
+ transpilerError(
93
+ [
94
+ "<$red:Validation Error:$> ",
95
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required argument(s):$> <$red:${missingKeys.join(", ")}$>`
96
+ ],
97
+ context
98
+ );
89
99
  }
90
100
  }
91
101
  if (includes && Array.isArray(includes)) {
@@ -93,11 +103,14 @@ const RulesValidationPlugin = {
93
103
  return !includes.includes(key) && !mapperFile.extraProps.has(key);
94
104
  });
95
105
  if (invalidKeys.length > 0) {
96
- transpilerError([
97
- "{line}<$red:Validation Error:$> ",
98
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:contains invalid argument key(s):$> <$red:${invalidKeys.join(", ")}$>`,
99
- `{N}<$yellow:Allowed keys are:$> <$green:${includes.join(", ")}$>{line}`
100
- ]);
106
+ transpilerError(
107
+ [
108
+ "<$red:Validation Error:$> ",
109
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:contains invalid argument key(s):$> <$red:${invalidKeys.join(", ")}$>`,
110
+ `{N}<$yellow:Allowed keys are:$> <$green:${includes.join(", ")}$>`
111
+ ],
112
+ context
113
+ );
101
114
  }
102
115
  }
103
116
  }
@@ -114,10 +127,13 @@ const RulesValidationPlugin = {
114
127
  if (keyPattern) {
115
128
  const invalidKeys = argKeys.filter(key => !keyPattern.test(key));
116
129
  if (invalidKeys.length > 0) {
117
- transpilerError([
118
- "{line}<$red:Validation Error:$> ",
119
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:contains argument keys that do not match pattern $> <$green:${keyPattern.toString()}$>: <$red:${invalidKeys.join(", ")}$>{line}`
120
- ]);
130
+ transpilerError(
131
+ [
132
+ "<$red:Validation Error:$> ",
133
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:contains argument keys that do not match pattern $> <$green:${keyPattern.toString()}$>: <$red:${invalidKeys.join(", ")}$>`
134
+ ],
135
+ context
136
+ );
121
137
  }
122
138
  }
123
139
  }
@@ -131,15 +147,21 @@ const RulesValidationPlugin = {
131
147
  const valueRule = rules.values[key];
132
148
  if (valueRule) {
133
149
  if (valueRule instanceof RegExp && !valueRule.test(value)) {
134
- transpilerError([
135
- "{line}<$red:Validation Error:$> ",
136
- `<$yellow:Argument key$> <$blue:'${key}'$> <$yellow:in$> <$blue:'${id}'$> <$yellow:has invalid value:$> <$red:'${value}'$>{N}<$yellow:Expected to match pattern:$> <$green:${valueRule.toString()}$>{line}`
137
- ]);
150
+ transpilerError(
151
+ [
152
+ "<$red:Validation Error:$> ",
153
+ `<$yellow:Argument key$> <$blue:'${key}'$> <$yellow:in$> <$blue:'${id}'$> <$yellow:has invalid value:$> <$red:'${value}'$>{N}<$yellow:Expected to match pattern:$> <$green:${valueRule.toString()}$>`
154
+ ],
155
+ context
156
+ );
138
157
  } else if (typeof valueRule === "function" && !valueRule(value)) {
139
- transpilerError([
140
- "{line}<$red:Validation Error:$> ",
141
- `<$yellow:Argument key$> <$blue:'${key}'$> <$yellow:in$> <$blue:'${id}'$> <$yellow:failed custom validation for value:$> <$red:'${value}'$>{line}`
142
- ]);
158
+ transpilerError(
159
+ [
160
+ "<$red:Validation Error:$> ",
161
+ `<$yellow:Argument key$> <$blue:'${key}'$> <$yellow:in$> <$blue:'${id}'$> <$yellow:failed custom validation for value:$> <$red:'${value}'$>`
162
+ ],
163
+ context
164
+ );
143
165
  }
144
166
  }
145
167
  }
@@ -153,16 +175,22 @@ const RulesValidationPlugin = {
153
175
  if (content !== undefined && rules.content) {
154
176
  const { maxLength, match } = rules.content;
155
177
  if (maxLength && content.length > maxLength) {
156
- transpilerError([
157
- "{line}<$red:Validation Error:$> ",
158
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:content exceeds maximum length of$> <$green:${maxLength}$> <$yellow:characters. Found$> <$red:${content.length}$>{line}`
159
- ]);
178
+ transpilerError(
179
+ [
180
+ "<$red:Validation Error:$> ",
181
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:content exceeds maximum length of$> <$green:${maxLength}$> <$yellow:characters. Found$> <$red:${content.length}$>`
182
+ ],
183
+ context
184
+ );
160
185
  }
161
186
  if (match && match instanceof RegExp && !match.test(content)) {
162
- transpilerError([
163
- "{line}<$red:Validation Error:$> ",
164
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:content does not match required pattern:$> <$green:${match.toString()}$>{line}`
165
- ]);
187
+ transpilerError(
188
+ [
189
+ "<$red:Validation Error:$> ",
190
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:content does not match required pattern:$> <$green:${match.toString()}$>`
191
+ ],
192
+ context
193
+ );
166
194
  }
167
195
  }
168
196
 
@@ -171,10 +199,13 @@ const RulesValidationPlugin = {
171
199
  // ========================================================================== //
172
200
  if (rules.is_self_closing && (type === "Block" || content)) {
173
201
  if (content) {
174
- transpilerError([
175
- "{line}<$red:Validation Error:$> ",
176
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is self-closing tag and is not allowed to have a content | children$>{line}`
177
- ]);
202
+ transpilerError(
203
+ [
204
+ "<$red:Validation Error:$> ",
205
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is self-closing tag and is not allowed to have a content | children$>`
206
+ ],
207
+ context
208
+ );
178
209
  }
179
210
  }
180
211
 
@@ -185,10 +216,13 @@ const RulesValidationPlugin = {
185
216
  if (typeToValidate && type !== "Text") {
186
217
  const allowedTypes = Array.isArray(typeToValidate) ? typeToValidate : [typeToValidate];
187
218
  if (!allowedTypes.includes("any") && !allowedTypes.includes(type)) {
188
- transpilerError([
189
- "{line}<$red:Validation Error:$> ",
190
- `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is expected to be type$> <$green:'${allowedTypes.join(" | ")}'$>{N}<$cyan:Received type: $> <$magenta:'${type}'$>{line}`
191
- ]);
219
+ transpilerError(
220
+ [
221
+ "<$red:Validation Error:$> ",
222
+ `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is expected to be type$> <$green:'${allowedTypes.join(" | ")}'$>{N}<$cyan:Received type: $> <$magenta:'${type}'$>`
223
+ ],
224
+ context
225
+ );
192
226
  }
193
227
  }
194
228
  }
@@ -56,11 +56,12 @@ async function generateOutput(ast, i, format, mapper_file) {
56
56
  // ========================================================================== //
57
57
  // Always use placeholders for blocks to support wrapping //
58
58
  // ========================================================================== //
59
- const placeholder = format === mdxFormat && node.body.length > 0 ? `\n${BODY_PLACEHOLDER}\n` : BODY_PLACEHOLDER;
59
+ const isParentBlock = format === mdxFormat && node.body.length > 1;
60
+ const placeholder = isParentBlock ? `\n${BODY_PLACEHOLDER}\n` : BODY_PLACEHOLDER;
60
61
  const textContent = getNodeText(node);
61
62
 
62
63
  result += target.render.call(mapper_file, { args: node.args, content: placeholder, textContent, ast: node });
63
- if (format === mdxFormat) result = "\n" + result + "\n";
64
+ if (isParentBlock) result = "\n" + result + "\n";
64
65
 
65
66
  // ========================================================================== //
66
67
  // Process body nodes recursively //
@@ -112,7 +113,12 @@ async function generateOutput(ast, i, format, mapper_file) {
112
113
 
113
114
  case BLOCK:
114
115
  const blockOutput = await generateOutput(body_node, i, format, mapper_file);
115
- context = context.trim() ? context.trimEnd() + "\n" + blockOutput : context + blockOutput;
116
+ const blockIsParent = format === mdxFormat && body_node.body.length > 1;
117
+ if (format === mdxFormat && !blockIsParent) {
118
+ context += blockOutput;
119
+ } else {
120
+ context = context.trim() ? context.trimEnd() + "\n" + blockOutput : context + blockOutput;
121
+ }
116
122
  break;
117
123
  }
118
124
  }
@@ -138,7 +144,8 @@ async function generateOutput(ast, i, format, mapper_file) {
138
144
  `<$yellow:Identifier$> <$blue:'${node.id}'$> <$yellow: is not found in mapping outputs$>{line}`
139
145
  ]);
140
146
  }
141
- return result.trimEnd() + "\n";
147
+ const newline = (format === mdxFormat && node.body.length <= 1) ? "" : "\n";
148
+ return result.trimEnd() + newline;
142
149
  }
143
150
 
144
151
  // ========================================================================== //
package/index.js CHANGED
@@ -32,6 +32,7 @@ class SomMark {
32
32
  this.priority = priority;
33
33
  this.filename = filename;
34
34
  this.warnings = [];
35
+ this._prepared = false;
35
36
 
36
37
  // 1. Identify which built-in plugins should be active by default
37
38
  const inactiveByDefault = ["raw-content", "sommark-format"];
@@ -188,29 +189,10 @@ class SomMark {
188
189
 
189
190
  return processed;
190
191
  }
192
+
193
+ _ensurePrepared() {
194
+ if (this._prepared) return;
191
195
 
192
- async lex(src = this.src) {
193
- if (src !== this.src) this.src = src;
194
- const processedSrc = await this._applyScopedPreprocessors(this.src);
195
- let tokens = lexer(processedSrc, this.filename);
196
- tokens = await this.pluginManager.runAfterLex(tokens);
197
- return tokens;
198
- }
199
-
200
- async parse(src = this.src) {
201
- const tokens = await this.lex(src);
202
- let ast = parser(tokens, this.filename);
203
- ast = await this.pluginManager.runOnAst(ast, {
204
- mapperFile: this.mapperFile,
205
- filename: this.filename,
206
- format: this.format,
207
- instance: this
208
- });
209
- return ast;
210
- }
211
-
212
- async transpile(src = this.src) {
213
- if (src !== this.src) this.src = src;
214
196
  // 1. Resolve Dynamic Formats from Plugins if built-in failed
215
197
  if (!this.mapperFile) {
216
198
  const PluginMapper = this.pluginManager.getFormatMapper(this.format);
@@ -230,8 +212,6 @@ class SomMark {
230
212
  // Run active registration hooks from plugins
231
213
  this.pluginManager.runRegisterHooks(this);
232
214
 
233
- const ast = await this.parse(src);
234
-
235
215
  // 2. Extend Mapper with static plugins definitions
236
216
  const extensions = this.pluginManager.getMapperExtensions();
237
217
  if (extensions.outputs.length > 0) {
@@ -256,6 +236,36 @@ class SomMark {
256
236
  }
257
237
  }
258
238
 
239
+ this._prepared = true;
240
+ }
241
+
242
+ async lex(src = this.src) {
243
+ this._ensurePrepared();
244
+ if (src !== this.src) this.src = src;
245
+ const processedSrc = await this._applyScopedPreprocessors(this.src);
246
+ let tokens = lexer(processedSrc, this.filename);
247
+ tokens = await this.pluginManager.runAfterLex(tokens);
248
+ return tokens;
249
+ }
250
+
251
+ async parse(src = this.src) {
252
+ const tokens = await this.lex(src);
253
+ let ast = parser(tokens, this.filename);
254
+ ast = await this.pluginManager.runOnAst(ast, {
255
+ mapperFile: this.mapperFile,
256
+ filename: this.filename,
257
+ format: this.format,
258
+ instance: this
259
+ });
260
+ return ast;
261
+ }
262
+
263
+ async transpile(src = this.src) {
264
+ if (src !== this.src) this.src = src;
265
+ this._ensurePrepared();
266
+
267
+ const ast = await this.parse(src);
268
+
259
269
  let result = await transpiler({ ast, format: this.format, mapperFile: this.mapperFile, includeDocument: this.includeDocument });
260
270
 
261
271
  // 3. Run Transformers
@@ -299,6 +309,8 @@ const lexSync = src => lexer(src);
299
309
 
300
310
  const parseSync = src => parser(lexer(src));
301
311
 
312
+ import { findAndLoadConfig } from "./core/helpers/config-loader.js";
313
+
302
314
  export {
303
315
  HTML,
304
316
  MARKDOWN,
@@ -320,6 +332,7 @@ export {
320
332
  list,
321
333
  parseList,
322
334
  safeArg,
323
- todo
335
+ todo,
336
+ findAndLoadConfig
324
337
  };
325
338
  export default SomMark;
@@ -147,7 +147,7 @@ HTML.register(
147
147
 
148
148
  return this.tag("pre").body(code_element.body(code));
149
149
  },
150
- { escape: false, type: "AtBlock" }
150
+ { escape: false, type: ["AtBlock", "Block"] }
151
151
  );
152
152
  // List
153
153
  HTML.register(
@@ -74,7 +74,7 @@ MARKDOWN.register(
74
74
  },
75
75
  {
76
76
  escape: false,
77
- type: "AtBlock"
77
+ type: ["AtBlock", "Block"]
78
78
  }
79
79
  );
80
80
  // Link
@@ -90,7 +90,7 @@ const { tag } = MDX;
90
90
  MDX.inherit(MARKDOWN);
91
91
 
92
92
  // Block for raw MDX content (ESM, etc.)
93
- MDX.register("mdx", ({ content }) => content, { escape: false, type: "Block" });
93
+ MDX.register("mdx", ({ content }) => content, { escape: false, type: ["AtBlock", "Block"] });
94
94
 
95
95
  // Re-register HTML tags to use jsxProps
96
96
  HTML_TAGS.forEach(tagName => {
package/mappers/mapper.js CHANGED
@@ -2,7 +2,7 @@ import TagBuilder from "../formatter/tag.js";
2
2
  import MarkdownBuilder from "../formatter/mark.js";
3
3
  import escapeHTML from "../helpers/escapeHTML.js";
4
4
  import { sommarkError } from "../core/errors.js";
5
- import { matchedValue, safeArg } from "../helpers/utils.js";
5
+ import { matchedValue, safeArg, htmlTable, list, todo } from "../helpers/utils.js";
6
6
 
7
7
 
8
8
  // ========================================================================== //
@@ -33,6 +33,9 @@ class Mapper {
33
33
  this.#customHeaderContent = "";
34
34
 
35
35
  this.escapeHTML = escapeHTML;
36
+ this.htmlTable = htmlTable;
37
+ this.list = list;
38
+ this.todo = todo;
36
39
  this.styles = [];
37
40
  }
38
41
 
@@ -202,7 +205,6 @@ class Mapper {
202
205
 
203
206
  return false;
204
207
  } catch (error) {
205
- console.error(error);
206
208
  return false;
207
209
  }
208
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sommark",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "description": "SomMark is a declarative, extensible markup language for structured content that can be converted to HTML, Markdown, MDX, JSON, and more.",
5
5
  "main": "index.js",
6
6
  "directories": {