sommark 4.0.0 → 4.0.1

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.
@@ -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} mappingFile - Custom mapped rules.
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, mappingFile, config) {
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, mappingFile, config });
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 mappingFile = config.mappingFile;
78
+ let mapperFile = config.mapperFile || config.mappingFile;
79
79
 
80
- if (!mappingFile) {
81
- mappingFile = format === "html" ? HTML : format === "markdown" ? MARKDOWN : format === "mdx" ? MDX : format === "json" ? Json : format === "xml" ? XML : null;
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, mappingFile, config);
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
 
@@ -15,22 +15,25 @@ 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, mappingFile = "", config = null }) {
18
+ export async function transpile({ src, format, filename = null, mapperFile = "", config = null }) {
19
19
  const finalConfig = config || await loadConfig(filename);
20
- let finalMapper = mappingFile;
20
+ let finalMapper = mapperFile;
21
21
 
22
22
  // 1. Find the Mapping File
23
- if (typeof mappingFile !== "object" || mappingFile === null) {
24
- if (finalConfig.mappingFile) {
25
- finalMapper = finalConfig.mappingFile;
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
34
  if (typeof finalMapper === "string" && finalMapper !== "" && (await isExist(finalMapper))) {
32
- const mappingFileURL = pathToFileURL(path.resolve(process.cwd(), finalMapper)).href;
33
- const loadedMapper = await import(mappingFileURL);
35
+ const mapperFileURL = pathToFileURL(path.resolve(process.cwd(), finalMapper)).href;
36
+ const loadedMapper = await import(mapperFileURL);
34
37
  finalMapper = loadedMapper.default;
35
38
  }
36
39
  }
@@ -62,14 +62,24 @@ export async function findAndLoadConfig(targetPath) {
62
62
  const defaultConfig = {
63
63
  outputFile: "output",
64
64
  outputDir: startDir,
65
- mappingFile: null,
65
+ mapperFile: null,
66
66
  removeComments: true,
67
+ placeholders: {},
68
+ customProps: [],
67
69
  };
68
70
 
69
71
  if (configPath) {
70
72
  const loadedConfig = await loadConfigFile(configPath);
71
73
  if (loadedConfig) {
72
- const finalConfig = { ...defaultConfig, ...loadedConfig, resolvedConfigPath: configPath };
74
+ // Support both mapperFile and mappingFile (backwards compatibility)
75
+ const finalMapper = loadedConfig.mapperFile || loadedConfig.mappingFile || defaultConfig.mapperFile;
76
+
77
+ const finalConfig = {
78
+ ...defaultConfig,
79
+ ...loadedConfig,
80
+ mapperFile: finalMapper,
81
+ resolvedConfigPath: configPath
82
+ };
73
83
  if (loadedConfig.outputDir) {
74
84
  const configDir = path.dirname(configPath);
75
85
  finalConfig.outputDir = path.resolve(configDir, loadedConfig.outputDir);
@@ -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
- bodyOutput = mapper_file ? mapper_file.text(text, target?.options) : text;
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
- let atContent = String(body_node.content || "").trim();
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, placeholder = {}, placeholders = {}, customProps = [], importStack = [] }) {
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 = { ...placeholder, ...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, placeholder = {}, placeholders = {}, customProps = [] } = options;
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", "placeholder", "placeholders", "customProps"];
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, placeholder, placeholders, customProps });
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, placeholder = {}, placeholders = {}, customProps = [] } = options;
323
- return new SomMark({ src, format, filename, mapperFile, removeComments, placeholder, placeholders, customProps }).parseSync();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sommark",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "SomMark is a declarative, extensible markup language for structured content that can be converted to HTML, Markdown, MDX, JSON, XML, and more.",
5
5
  "main": "index.js",
6
6
  "files": [