sommark 4.0.0 → 4.0.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/cli/commands/build.js +7 -7
- package/cli/helpers/transpile.js +17 -9
- package/core/helpers/config-loader.js +25 -4
- package/core/transpiler.js +7 -6
- package/helpers/dedent.js +19 -0
- package/index.js +7 -19
- package/package.json +1 -1
package/cli/commands/build.js
CHANGED
|
@@ -20,14 +20,14 @@ import { transpile } from "../helpers/transpile.js";
|
|
|
20
20
|
* @param {string} outputFile - Output filename (without extension).
|
|
21
21
|
* @param {string} format - Target format (html, markdown, etc.).
|
|
22
22
|
* @param {string} sourcePath - Path to the source .smark file.
|
|
23
|
-
* @param {Mapper|null}
|
|
23
|
+
* @param {Mapper|null} mapperFile - Custom mapped rules.
|
|
24
24
|
* @returns {Promise<string>} - The full path to the created file.
|
|
25
25
|
*/
|
|
26
|
-
async function generateOutput(outputDir, outputFile, format, sourcePath,
|
|
26
|
+
async function generateOutput(outputDir, outputFile, format, sourcePath, mapperFile, config) {
|
|
27
27
|
let source_code = await readContent(sourcePath);
|
|
28
28
|
source_code = source_code.toString();
|
|
29
29
|
const absolutePath = path.resolve(process.cwd(), sourcePath);
|
|
30
|
-
const output = await transpile({ src: source_code, format, filename: absolutePath,
|
|
30
|
+
const output = await transpile({ src: source_code, format, filename: absolutePath, mapperFile, config });
|
|
31
31
|
const finalPath = path.join(outputDir, `${outputFile}.${extensions[format]}`);
|
|
32
32
|
await createFile(outputDir, `${outputFile}.${extensions[format]}`, output);
|
|
33
33
|
return finalPath;
|
|
@@ -75,10 +75,10 @@ export async function runBuild(format_option, sourcePath, outputFlag, outputFile
|
|
|
75
75
|
// Configuration for output
|
|
76
76
|
let finalOutputFile = config.outputFile;
|
|
77
77
|
let finalOutputDir = config.outputDir;
|
|
78
|
-
let
|
|
78
|
+
let mapperFile = config.mapperFile || config.mappingFile;
|
|
79
79
|
|
|
80
|
-
if (!
|
|
81
|
-
|
|
80
|
+
if (!mapperFile) {
|
|
81
|
+
mapperFile = format === "html" ? HTML : format === "markdown" ? MARKDOWN : format === "mdx" ? MDX : format === "json" ? Json : format === "xml" ? XML : null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// CLI Overrides
|
|
@@ -89,7 +89,7 @@ export async function runBuild(format_option, sourcePath, outputFlag, outputFile
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
const createdFilePath = await generateOutput(finalOutputDir, finalOutputFile, format, sourcePath,
|
|
92
|
+
const createdFilePath = await generateOutput(finalOutputDir, finalOutputFile, format, sourcePath, mapperFile, config);
|
|
93
93
|
const stats = await fs.stat(createdFilePath);
|
|
94
94
|
const date = new Date().toLocaleString();
|
|
95
95
|
|
package/cli/helpers/transpile.js
CHANGED
|
@@ -15,23 +15,31 @@ const default_mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [m
|
|
|
15
15
|
// ========================================================================== //
|
|
16
16
|
// Transpile Function //
|
|
17
17
|
// ========================================================================== //
|
|
18
|
-
export async function transpile({ src, format, filename = null,
|
|
18
|
+
export async function transpile({ src, format, filename = null, mapperFile = "", config = null }) {
|
|
19
19
|
const finalConfig = config || await loadConfig(filename);
|
|
20
|
-
let finalMapper =
|
|
20
|
+
let finalMapper = mapperFile;
|
|
21
21
|
|
|
22
22
|
// 1. Find the Mapping File
|
|
23
|
-
if (typeof
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (typeof mapperFile !== "object" || mapperFile === null) {
|
|
24
|
+
// Support both names from config
|
|
25
|
+
const configMapper = finalConfig.mapperFile || finalConfig.mappingFile;
|
|
26
|
+
|
|
27
|
+
if (configMapper) {
|
|
28
|
+
finalMapper = configMapper;
|
|
26
29
|
} else {
|
|
27
30
|
finalMapper = default_mapperFiles[format];
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// Custom Mapper (String Path)
|
|
31
|
-
if (typeof finalMapper === "string" && finalMapper !== ""
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
34
|
+
if (typeof finalMapper === "string" && finalMapper !== "") {
|
|
35
|
+
const baseDir = finalConfig.resolvedConfigPath ? path.dirname(finalConfig.resolvedConfigPath) : process.cwd();
|
|
36
|
+
const absoluteMapperPath = path.resolve(baseDir, finalMapper);
|
|
37
|
+
|
|
38
|
+
if (await isExist(absoluteMapperPath)) {
|
|
39
|
+
const mapperFileURL = `${pathToFileURL(absoluteMapperPath).href}?t=${Date.now()}`;
|
|
40
|
+
const loadedMapper = await import(mapperFileURL);
|
|
41
|
+
finalMapper = loadedMapper.default;
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -48,28 +48,49 @@ export async function findAndLoadConfig(targetPath) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// 2. Check the
|
|
51
|
+
// 2. Check the target directory
|
|
52
52
|
if (!configPath) {
|
|
53
53
|
const localConfig = path.join(startDir, CONFIG_FILE_NAME);
|
|
54
54
|
try {
|
|
55
55
|
await fs.access(localConfig);
|
|
56
56
|
configPath = localConfig;
|
|
57
57
|
} catch {
|
|
58
|
-
// No local config found
|
|
58
|
+
// No local config found in target dir
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
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);
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(cwdConfig);
|
|
67
|
+
configPath = cwdConfig;
|
|
68
|
+
} catch {
|
|
69
|
+
// No config found in CWD
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
const defaultConfig = {
|
|
63
74
|
outputFile: "output",
|
|
64
75
|
outputDir: startDir,
|
|
65
|
-
|
|
76
|
+
mapperFile: null,
|
|
66
77
|
removeComments: true,
|
|
78
|
+
placeholders: {},
|
|
79
|
+
customProps: [],
|
|
67
80
|
};
|
|
68
81
|
|
|
69
82
|
if (configPath) {
|
|
70
83
|
const loadedConfig = await loadConfigFile(configPath);
|
|
71
84
|
if (loadedConfig) {
|
|
72
|
-
|
|
85
|
+
// Support both mapperFile and mappingFile (backwards compatibility)
|
|
86
|
+
const finalMapper = loadedConfig.mapperFile || loadedConfig.mappingFile || defaultConfig.mapperFile;
|
|
87
|
+
|
|
88
|
+
const finalConfig = {
|
|
89
|
+
...defaultConfig,
|
|
90
|
+
...loadedConfig,
|
|
91
|
+
mapperFile: finalMapper,
|
|
92
|
+
resolvedConfigPath: configPath
|
|
93
|
+
};
|
|
73
94
|
if (loadedConfig.outputDir) {
|
|
74
95
|
const configDir = path.dirname(configPath);
|
|
75
96
|
finalConfig.outputDir = path.resolve(configDir, loadedConfig.outputDir);
|
package/core/transpiler.js
CHANGED
|
@@ -2,6 +2,7 @@ import { BLOCK, TEXT, INLINE, ATBLOCK, COMMENT } from "./labels.js";
|
|
|
2
2
|
import { transpilerError } from "./errors.js";
|
|
3
3
|
import { textFormat, htmlFormat, markdownFormat, mdxFormat, xmlFormat } from "./formats.js";
|
|
4
4
|
import { matchedValue } from "../helpers/utils.js";
|
|
5
|
+
import { dedentBy } from "../helpers/dedent.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* SomMark Transpiler
|
|
@@ -84,7 +85,7 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
84
85
|
const textContent = getNodeText(node);
|
|
85
86
|
|
|
86
87
|
let content = (node.body?.length === 0) ? "" :
|
|
87
|
-
(node.type === ATBLOCK ? (node.content || "") :
|
|
88
|
+
(node.type === ATBLOCK ? dedentBy(node.content || "", node.range?.start?.character || 0).trim() :
|
|
88
89
|
(node.type === INLINE ? (node.value || "") : BODY_PLACEHOLDER));
|
|
89
90
|
|
|
90
91
|
// Apply pipelines to format literal values
|
|
@@ -132,7 +133,9 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
132
133
|
switch (body_node.type) {
|
|
133
134
|
case TEXT:
|
|
134
135
|
const text = String(body_node.text || "");
|
|
135
|
-
|
|
136
|
+
// Dedent text relative to the parent block's indentation
|
|
137
|
+
const localDedentedText = dedentBy(text, node.range?.start?.character || 0);
|
|
138
|
+
bodyOutput = mapper_file ? mapper_file.text(localDedentedText, target?.options) : localDedentedText;
|
|
136
139
|
break;
|
|
137
140
|
|
|
138
141
|
case INLINE:
|
|
@@ -160,15 +163,14 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
160
163
|
break;
|
|
161
164
|
|
|
162
165
|
case ATBLOCK:
|
|
163
|
-
console.log(`[TRANSPILER] Processing ATBLOCK: ${body_node.id}`);
|
|
164
166
|
let atTarget = matchedValue(mapper_file.outputs, body_node.id);
|
|
165
167
|
if (!atTarget) {
|
|
166
168
|
atTarget = mapper_file.getUnknownTag(body_node);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
|
|
171
|
+
// AtBlocks handle their own absolute dedenting
|
|
172
|
+
let atContent = dedentBy(body_node.content || "", body_node.range?.start?.character || 0).trim();
|
|
170
173
|
if (mapper_file) {
|
|
171
|
-
console.log(`[TRANSPILER] Calling atBlockBody for ${body_node.id}`);
|
|
172
174
|
atContent = mapper_file.atBlockBody(atContent, atTarget?.options || {});
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -195,7 +197,6 @@ async function generateOutput(ast, i, format, mapper_file) {
|
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
// Trim only leading/trailing newlines and their surrounding spaces to preserve indentation
|
|
199
200
|
const finalContext = effectiveTrimAndWrap ? context.replace(/^\s*[\r\n]+|[\r\n]+\s*$/g, "") : context;
|
|
200
201
|
|
|
201
202
|
if (result.includes(BODY_PLACEHOLDER)) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedents a string by a given amount of characters.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} str - The string to dedent.
|
|
5
|
+
* @param {number} amount - The number of characters to remove from the start of each line.
|
|
6
|
+
* @returns {string} - The dedented string.
|
|
7
|
+
*/
|
|
8
|
+
export function dedentBy(str, amount) {
|
|
9
|
+
if (!str || amount <= 0) return str;
|
|
10
|
+
const lines = str.split("\n");
|
|
11
|
+
const dedentedLines = lines.map((line) => {
|
|
12
|
+
let count = 0;
|
|
13
|
+
while (count < amount && (line[count] === " " || line[count] === "\t")) {
|
|
14
|
+
count++;
|
|
15
|
+
}
|
|
16
|
+
return line.slice(count);
|
|
17
|
+
});
|
|
18
|
+
return dedentedLines.join("\n");
|
|
19
|
+
}
|
package/index.js
CHANGED
|
@@ -34,17 +34,16 @@ class SomMark {
|
|
|
34
34
|
* @param {string} [options.filename="anonymous"] - The name of the file, used for errors and settings.
|
|
35
35
|
* @param {boolean} [options.removeComments=true] - If true, comments will be removed from the final code.
|
|
36
36
|
* @param {Object} [options.placeholders={}] - Values to use for {placeholders}.
|
|
37
|
-
* @param {Object} [options.placeholder={}] - Alias for placeholders (backward compatibility).
|
|
38
37
|
* @param {Array<string>} [options.customProps=[]] - Allowed custom HTML attributes.
|
|
39
38
|
* @param {Array<string>} [options.importStack=[]] - Tracking for circular dependencies.
|
|
40
39
|
*/
|
|
41
|
-
constructor({ src, format, mapperFile = null, filename = "anonymous", removeComments = true,
|
|
40
|
+
constructor({ src, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], importStack = [] }) {
|
|
42
41
|
this.src = src;
|
|
43
42
|
this.targetFormat = format;
|
|
44
43
|
this.mapperFile = mapperFile;
|
|
45
44
|
this.filename = filename;
|
|
46
45
|
this.removeComments = removeComments;
|
|
47
|
-
this.placeholders =
|
|
46
|
+
this.placeholders = placeholders;
|
|
48
47
|
this.customProps = customProps;
|
|
49
48
|
this.importStack = importStack;
|
|
50
49
|
this.warnings = [];
|
|
@@ -81,16 +80,6 @@ class SomMark {
|
|
|
81
80
|
this._initializeMappers();
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
/**
|
|
85
|
-
* Backward compatibility alias for placeholders.
|
|
86
|
-
*/
|
|
87
|
-
get placeholder() {
|
|
88
|
-
return this.placeholders;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
set placeholder(val) {
|
|
92
|
-
this.placeholders = val;
|
|
93
|
-
}
|
|
94
83
|
|
|
95
84
|
/**
|
|
96
85
|
* Adds a new rule or changes an existing one.
|
|
@@ -278,16 +267,15 @@ async function parse(src, filename = "anonymous") {
|
|
|
278
267
|
* @param {Mapper|null} [options.mapperFile=null] - Custom rules for formatting.
|
|
279
268
|
* @param {boolean} [options.removeComments=true] - Strip comments.
|
|
280
269
|
* @param {Object} [options.placeholders={}] - Global placeholders.
|
|
281
|
-
* @param {Object} [options.placeholder={}] - Alias for placeholders.
|
|
282
270
|
* @param {Array<string>} [options.customProps=[]] - Custom attribute whitelist.
|
|
283
271
|
* @returns {Promise<string>} - Transpiled output.
|
|
284
272
|
*/
|
|
285
273
|
async function transpile(options = {}) {
|
|
286
|
-
const { src, format = htmlFormat, filename = "anonymous", mapperFile = null, removeComments = true,
|
|
274
|
+
const { src, format = htmlFormat, filename = "anonymous", mapperFile = null, removeComments = true, placeholders = {}, customProps = [] } = options;
|
|
287
275
|
if (typeof options !== "object" || options === null) {
|
|
288
276
|
runtimeError([`{line}<$red:Invalid Options:$> <$yellow:The options argument must be a non-null object.$>{line}`]);
|
|
289
277
|
}
|
|
290
|
-
const knownProps = ["src", "format", "filename", "mapperFile", "removeComments", "
|
|
278
|
+
const knownProps = ["src", "format", "filename", "mapperFile", "removeComments", "placeholders", "customProps"];
|
|
291
279
|
Object.keys(options).forEach(key => {
|
|
292
280
|
if (!knownProps.includes(key)) {
|
|
293
281
|
runtimeError([
|
|
@@ -299,7 +287,7 @@ async function transpile(options = {}) {
|
|
|
299
287
|
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for transpilation.$>{line}`]);
|
|
300
288
|
}
|
|
301
289
|
|
|
302
|
-
const sm = new SomMark({ src, format, filename, mapperFile, removeComments,
|
|
290
|
+
const sm = new SomMark({ src, format, filename, mapperFile, removeComments, placeholders, customProps });
|
|
303
291
|
return await sm.transpile();
|
|
304
292
|
}
|
|
305
293
|
|
|
@@ -319,8 +307,8 @@ const lexSync = src => lexer(src);
|
|
|
319
307
|
* @returns {Array<Object>} - The code tree.
|
|
320
308
|
*/
|
|
321
309
|
const parseSync = (src, options = {}) => {
|
|
322
|
-
const { format = htmlFormat, filename = "anonymous", mapperFile = null, removeComments = true,
|
|
323
|
-
return new SomMark({ src, format, filename, mapperFile, removeComments,
|
|
310
|
+
const { format = htmlFormat, filename = "anonymous", mapperFile = null, removeComments = true, placeholders = {}, customProps = [] } = options;
|
|
311
|
+
return new SomMark({ src, format, filename, mapperFile, removeComments, placeholders, customProps }).parseSync();
|
|
324
312
|
};
|
|
325
313
|
|
|
326
314
|
import { findAndLoadConfig } from "./core/helpers/config-loader.js";
|
package/package.json
CHANGED