sommark 3.3.0 → 3.3.2
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 +0 -2
- package/cli/commands/build.js +1 -1
- package/cli/commands/init.js +3 -0
- package/cli/commands/print.js +2 -2
- package/cli/helpers/config.js +6 -51
- package/cli/helpers/transpile.js +1 -1
- package/core/errors.js +61 -39
- package/core/helpers/config-loader.js +101 -0
- package/core/plugins/rules-validation-plugin.js +83 -49
- package/core/plugins/sommark-format.js +15 -2
- package/index.js +38 -25
- package/mappers/mapper.js +4 -2
- package/package.json +1 -1
- package/test_format_fix.js +42 -0
package/SOMMARK-SPEC.md
CHANGED
package/cli/commands/build.js
CHANGED
|
@@ -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(
|
package/cli/commands/init.js
CHANGED
|
@@ -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
|
// ======================================================
|
package/cli/commands/print.js
CHANGED
|
@@ -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({
|
package/cli/helpers/config.js
CHANGED
|
@@ -1,62 +1,17 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
16
|
+
return resolvedConfigPath;
|
|
62
17
|
}
|
package/cli/helpers/transpile.js
CHANGED
|
@@ -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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
}
|
|
@@ -54,6 +54,11 @@ export default {
|
|
|
54
54
|
// ========================================================================== //
|
|
55
55
|
// 2. Formatting Logic //
|
|
56
56
|
// ========================================================================== //
|
|
57
|
+
const shouldQuote = (val) => {
|
|
58
|
+
if (typeof val !== "string") return false;
|
|
59
|
+
return /[ \t\n\r,:[\]()@#]/.test(val);
|
|
60
|
+
};
|
|
61
|
+
|
|
57
62
|
const formatArgs = (args, type) => {
|
|
58
63
|
if (!args || args.length === 0) return "";
|
|
59
64
|
let usedKeys = new Set();
|
|
@@ -73,8 +78,15 @@ export default {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
let escapedVal = escapeArg(val, type);
|
|
81
|
+
if (shouldQuote(val)) {
|
|
82
|
+
const quotedVal = String(val)
|
|
83
|
+
.replace(/\\/g, "\\\\")
|
|
84
|
+
.replace(/"/g, '\\"');
|
|
85
|
+
escapedVal = `"${quotedVal}"`;
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
if (matchedKey) {
|
|
77
|
-
formattedArgs.push(`${matchedKey}
|
|
89
|
+
formattedArgs.push(`${matchedKey}: ${escapedVal}`);
|
|
78
90
|
} else {
|
|
79
91
|
formattedArgs.push(escapedVal);
|
|
80
92
|
}
|
|
@@ -183,7 +195,8 @@ export default {
|
|
|
183
195
|
result += `${innerIndentStr}[end]\n`;
|
|
184
196
|
} else if (child.type === "AtBlock") {
|
|
185
197
|
const argsStr = formatArgs(child.args, "AtBlock");
|
|
186
|
-
|
|
198
|
+
const atHeader = argsStr ? `@_${child.id}_@${argsStr}` : `@_${child.id}_@;`;
|
|
199
|
+
result += `${innerIndentStr}${atHeader}\n`;
|
|
187
200
|
if (child.content) {
|
|
188
201
|
// ========================================================================== //
|
|
189
202
|
// Remove leading spaces from messy text block and re-indent //
|
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;
|
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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import SomMark from './index.js';
|
|
2
|
+
|
|
3
|
+
async function test() {
|
|
4
|
+
const text = '[Block = attr: "val with space"]\n Content\n[end]\n';
|
|
5
|
+
const sm = new SomMark({
|
|
6
|
+
src: text,
|
|
7
|
+
format: 'html',
|
|
8
|
+
plugins: ['sommark-format']
|
|
9
|
+
});
|
|
10
|
+
await sm.parse();
|
|
11
|
+
const formatPlugin = sm.plugins.find(p => p.name === 'sommark-format');
|
|
12
|
+
const formatted = formatPlugin.formattedSource;
|
|
13
|
+
console.log("Original:\n", text);
|
|
14
|
+
console.log("Formatted:\n", formatted);
|
|
15
|
+
|
|
16
|
+
if (formatted.includes('"val with space"')) {
|
|
17
|
+
console.log("PASS: Quotes preserved for space-containing value.");
|
|
18
|
+
} else {
|
|
19
|
+
console.error("FAIL: Quotes LOST!");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const text2 = 'Text at top level\n(inline)->(bold)\n@_AtBlock_@;\n content\n@_end_@\n';
|
|
24
|
+
const sm2 = new SomMark({
|
|
25
|
+
src: text2,
|
|
26
|
+
format: 'html',
|
|
27
|
+
plugins: ['sommark-format']
|
|
28
|
+
});
|
|
29
|
+
await sm2.parse();
|
|
30
|
+
const formatted2 = sm2.plugins.find(p => p.name === 'sommark-format').formattedSource;
|
|
31
|
+
console.log("Top-level Original:\n", text2);
|
|
32
|
+
console.log("Top-level Formatted:\n", formatted2);
|
|
33
|
+
|
|
34
|
+
if (formatted2.includes('Text at top level') && formatted2.includes('(inline)->(bold)')) {
|
|
35
|
+
console.log("PASS: Top-level content preserved.");
|
|
36
|
+
} else {
|
|
37
|
+
console.error("FAIL: Top-level content LOST or corrupted!");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test();
|