mdstitch 1.0.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/bin/cli.js +47 -0
- package/package.json +19 -0
- package/src/commands/compress.js +22 -0
- package/src/commands/compress_helpers.js +127 -0
- package/src/commands/pretty.js +18 -0
- package/src/commands/pretty_helpers.js +66 -0
- package/src/commands/run.js +38 -0
- package/src/commands/run_helpers.js +128 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { program } = require('commander');
|
|
3
|
+
|
|
4
|
+
const { pretty } = require('../src/commands/pretty');
|
|
5
|
+
const { compress } = require('../src/commands/compress');
|
|
6
|
+
const { run } = require('../src/commands/run');
|
|
7
|
+
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
|
|
10
|
+
//setup
|
|
11
|
+
program
|
|
12
|
+
.name('agmod')
|
|
13
|
+
.description('A CLI tool for agmod')
|
|
14
|
+
.version(pkg.version); // enables: betterag --version
|
|
15
|
+
|
|
16
|
+
//Commands
|
|
17
|
+
program
|
|
18
|
+
.command('pretty')
|
|
19
|
+
.description('Format and prettify a agent.md file')
|
|
20
|
+
.argument('<file>', 'The file to prettify')
|
|
21
|
+
.action((file) => {
|
|
22
|
+
pretty(file);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// betterag compress <file>
|
|
26
|
+
program
|
|
27
|
+
.command('compress')
|
|
28
|
+
.description('Compress a file')
|
|
29
|
+
.argument('<file>', 'The file to compress')
|
|
30
|
+
.action((file) => {
|
|
31
|
+
compress(file);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// betterag run <file>
|
|
35
|
+
program
|
|
36
|
+
.command('run')
|
|
37
|
+
.description('Process @excel() references in a markdown file')
|
|
38
|
+
.argument('<file>', 'The markdown file to process')
|
|
39
|
+
.action((file) => {
|
|
40
|
+
run(file);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Parse ──────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
// Reads what the user typed in the terminal and runs the matching command
|
|
46
|
+
// This must always be the last line
|
|
47
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mdstitch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI tool for agmod",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agmd": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["cli", "agent", "markdown", "compress", "excel"],
|
|
12
|
+
"author": "tannergill",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^14.0.3",
|
|
17
|
+
"xlsx": "^0.18.5"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { compressFile } = require("./compress_helpers");
|
|
4
|
+
|
|
5
|
+
function compress(file) {
|
|
6
|
+
const inputPath = path.resolve(file);
|
|
7
|
+
const outputPath = inputPath.replace(/\.md$/, ".compressed.md");
|
|
8
|
+
|
|
9
|
+
const content = fs.readFileSync(inputPath, "utf8");
|
|
10
|
+
const compressed = compressFile(content);
|
|
11
|
+
|
|
12
|
+
fs.writeFileSync(outputPath, compressed, "utf8");
|
|
13
|
+
|
|
14
|
+
const originalSize = Buffer.byteLength(content, "utf8");
|
|
15
|
+
const compressedSize = Buffer.byteLength(compressed, "utf8");
|
|
16
|
+
const reduction = (((originalSize - compressedSize) / originalSize) * 100).toFixed(1);
|
|
17
|
+
|
|
18
|
+
console.log(`Compressed: ${inputPath} → ${outputPath}`);
|
|
19
|
+
console.log(`Size: ${originalSize} bytes → ${compressedSize} bytes (${reduction}% reduction)`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { compress };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
function parseFile(file) {
|
|
2
|
+
let splitFile = file.split(/\r?\n/)
|
|
3
|
+
let mapedFile = splitFile.map((line, index) => ({
|
|
4
|
+
index,
|
|
5
|
+
raw: line,
|
|
6
|
+
text: line.trim(),
|
|
7
|
+
}));
|
|
8
|
+
return mapedFile
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeStructure(lines) {
|
|
12
|
+
const relevantLines = lines.filter(line => line.text.length > 0);
|
|
13
|
+
const result = [];
|
|
14
|
+
|
|
15
|
+
for (const line of relevantLines) {
|
|
16
|
+
if (/^#{1,6}\s+/.test(line.text)) {
|
|
17
|
+
const level = line.text.match(/^#+/)[0].length;
|
|
18
|
+
result.push({ type: "heading", level, value: line.text.replace(/^#{1,6}\s+/, "") });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (/^[-*]\s+/.test(line.text)) {
|
|
22
|
+
result.push({ type: "bullet", value: line.text.replace(/^[-*]\s+/, "") });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
result.push({ type: "paragraph", value: line.text });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const fluffy = [
|
|
33
|
+
"so that",
|
|
34
|
+
"in order to",
|
|
35
|
+
"to ensure",
|
|
36
|
+
"to make sure",
|
|
37
|
+
"to allow",
|
|
38
|
+
"to avoid",
|
|
39
|
+
"to prevent",
|
|
40
|
+
"to help",
|
|
41
|
+
"to enable",
|
|
42
|
+
"make sure to",
|
|
43
|
+
"be sure to",
|
|
44
|
+
"don't forget to",
|
|
45
|
+
"always remember to",
|
|
46
|
+
"remember to",
|
|
47
|
+
"try to",
|
|
48
|
+
"feel free to",
|
|
49
|
+
"just",
|
|
50
|
+
"simply",
|
|
51
|
+
"easily",
|
|
52
|
+
"quickly",
|
|
53
|
+
"even if nobody asked",
|
|
54
|
+
"before you merge",
|
|
55
|
+
"and then",
|
|
56
|
+
"after that",
|
|
57
|
+
"from there",
|
|
58
|
+
"at that point",
|
|
59
|
+
"the",
|
|
60
|
+
"a",
|
|
61
|
+
"an",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function stripContent(text) {
|
|
65
|
+
let s = text
|
|
66
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
67
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
68
|
+
.replace(/\s*\([^)]{10,}\)/g, "");
|
|
69
|
+
|
|
70
|
+
for (const word of fluffy) {
|
|
71
|
+
const pattern = new RegExp(`\\b${word}\\b\\s*`, "gi");
|
|
72
|
+
s = s.replace(pattern, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return s.replace(/\s{2,}/g, " ").trim().replace(/[.,;]+$/, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildCompressed(nodes) {
|
|
79
|
+
let title = "";
|
|
80
|
+
const sections = [];
|
|
81
|
+
let currentSection = null;
|
|
82
|
+
|
|
83
|
+
let headingPath = [];
|
|
84
|
+
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
if (node.type === "heading" && node.level === 1) {
|
|
87
|
+
title = `[${node.value}]`;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (node.type === "heading") {
|
|
92
|
+
headingPath = headingPath.slice(0, node.level - 2);
|
|
93
|
+
headingPath.push(node.value.replace(/:$/, ""));
|
|
94
|
+
|
|
95
|
+
currentSection = { name: headingPath.join(">"), items: [] };
|
|
96
|
+
sections.push(currentSection);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ((node.type === "bullet" || node.type === "numbered") && currentSection) {
|
|
101
|
+
currentSection.items.push(stripContent(node.value));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (node.type === "paragraph" && currentSection) {
|
|
106
|
+
currentSection.items.push(stripContent(node.value));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lines = sections
|
|
112
|
+
.filter(section => section.items.length > 0)
|
|
113
|
+
.map(section => {
|
|
114
|
+
const items = section.items.join(";");
|
|
115
|
+
return `|${section.name}:{${items}}`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return [title, ...lines].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function compressFile(fileContent) {
|
|
122
|
+
const lines = parseFile(fileContent);
|
|
123
|
+
const nodes = normalizeStructure(lines);
|
|
124
|
+
return buildCompressed(nodes);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { compressFile };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { parseCompressed, buildPretty } = require("./pretty_helpers");
|
|
4
|
+
|
|
5
|
+
function pretty(file) {
|
|
6
|
+
const inputPath = path.resolve(file);
|
|
7
|
+
const outputPath = inputPath.replace(/\.compressed\.md$/, ".pretty.md")
|
|
8
|
+
.replace(/(?<!\.pretty)\.md$/, ".pretty.md");
|
|
9
|
+
|
|
10
|
+
const content = fs.readFileSync(inputPath, "utf8");
|
|
11
|
+
const parsed = parseCompressed(content);
|
|
12
|
+
const result = buildPretty(parsed);
|
|
13
|
+
|
|
14
|
+
fs.writeFileSync(outputPath, result, "utf8");
|
|
15
|
+
console.log(`Prettified: ${inputPath} → ${outputPath}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { pretty };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function parseCompressed(content) {
|
|
2
|
+
const result = { title: "", sections: [] };
|
|
3
|
+
const entries = content.split(/\n(?=\||\[)/);
|
|
4
|
+
|
|
5
|
+
for (const entry of entries) {
|
|
6
|
+
const trimmed = entry.trim();
|
|
7
|
+
if (!trimmed) continue;
|
|
8
|
+
|
|
9
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
10
|
+
result.title = trimmed.slice(1, -1);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!trimmed.startsWith("|")) continue;
|
|
15
|
+
|
|
16
|
+
const body = trimmed.slice(1);
|
|
17
|
+
|
|
18
|
+
const match = body.match(/^(.+?):\{(.+)\}$/s);
|
|
19
|
+
if (match) {
|
|
20
|
+
result.sections.push({
|
|
21
|
+
name: match[1].trim(),
|
|
22
|
+
items: match[2].split(";").map(i => i.trim()).filter(Boolean),
|
|
23
|
+
});
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
result.sections.push({ name: body.trim(), items: [] });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildPretty(parsed) {
|
|
34
|
+
const blocks = [];
|
|
35
|
+
let lastPath = [];
|
|
36
|
+
|
|
37
|
+
if (parsed.title) {
|
|
38
|
+
blocks.push(`# ${parsed.title}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const section of parsed.sections) {
|
|
42
|
+
const parts = section.name.split(">");
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
45
|
+
if (parts[i] !== lastPath[i]) {
|
|
46
|
+
// Only h2 gets a blank line before it
|
|
47
|
+
const prefix = i === 0 ? "\n" : "";
|
|
48
|
+
blocks.push(`${prefix}${"#".repeat(i + 2)} ${parts[i]}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const depth = parts.length;
|
|
53
|
+
const prefix = depth === 1 ? "\n" : "";
|
|
54
|
+
blocks.push(`${prefix}${"#".repeat(depth + 1)} ${parts[parts.length - 1]}`);
|
|
55
|
+
|
|
56
|
+
lastPath = parts;
|
|
57
|
+
|
|
58
|
+
if (section.items.length > 0) {
|
|
59
|
+
blocks.push(section.items.map(item => `- ${item}`).join("\n"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return blocks.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { parseCompressed, buildPretty };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { resolveExcelPath, describeSheet } = require("./run_helpers");
|
|
4
|
+
|
|
5
|
+
function run(file) {
|
|
6
|
+
const inputPath = path.resolve(file);
|
|
7
|
+
const outputPath = inputPath.replace(/\.md$/, ".processed.md");
|
|
8
|
+
const dir = path.dirname(inputPath);
|
|
9
|
+
|
|
10
|
+
let content = fs.readFileSync(inputPath, "utf8");
|
|
11
|
+
|
|
12
|
+
const pattern = /@excel\(([^)]+)\)/g;
|
|
13
|
+
let found = 0;
|
|
14
|
+
|
|
15
|
+
content = content.replace(pattern, (original, args) => {
|
|
16
|
+
const [fileRef, sheetName, range] = args.split(",").map(s => s.trim());
|
|
17
|
+
|
|
18
|
+
if (!fileRef || !sheetName || !range) {
|
|
19
|
+
console.warn(` Warning: @excel() requires all 3 arguments — @excel(filename, sheet, range)`);
|
|
20
|
+
return original;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const excelPath = resolveExcelPath(dir, fileRef);
|
|
24
|
+
if (!excelPath) {
|
|
25
|
+
console.warn(` Warning: Excel file not found — ${fileRef}`);
|
|
26
|
+
return `[Excel file not found: ${fileRef}]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
found++;
|
|
30
|
+
console.log(` Processing: ${fileRef} | sheet: ${sheetName} | range: ${range}`);
|
|
31
|
+
return describeSheet(excelPath, sheetName, range);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(outputPath, content, "utf8");
|
|
35
|
+
console.log(`\nProcessed ${found} @excel() reference(s): ${inputPath} → ${outputPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { run };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const XLSX = require("xlsx");
|
|
4
|
+
|
|
5
|
+
function resolveExcelPath(dir, name) {
|
|
6
|
+
const candidates = [
|
|
7
|
+
path.resolve(dir, name),
|
|
8
|
+
path.resolve(dir, `${name}.xlsx`),
|
|
9
|
+
path.resolve(dir, `${name}.xls`),
|
|
10
|
+
];
|
|
11
|
+
return candidates.find(fs.existsSync) || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function colToIndex(col) {
|
|
15
|
+
let index = 0;
|
|
16
|
+
for (let i = 0; i < col.length; i++) {
|
|
17
|
+
index = index * 26 + col.charCodeAt(i) - 64;
|
|
18
|
+
}
|
|
19
|
+
return index - 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function humanizeFormula(formula, headers, rangeColStart) {
|
|
23
|
+
return formula
|
|
24
|
+
.replace(/([A-Za-z0-9_]+)!([A-Z]+)(\d+)/g, (_, sheet, col) => {
|
|
25
|
+
const idx = colToIndex(col) - rangeColStart;
|
|
26
|
+
const header = headers[idx];
|
|
27
|
+
return header ? `[${sheet}:${header}]` : `[${sheet}:${col}]`;
|
|
28
|
+
})
|
|
29
|
+
.replace(/([A-Z]+)(\d+)/g, (match, col) => {
|
|
30
|
+
const idx = colToIndex(col) - rangeColStart;
|
|
31
|
+
const header = headers[idx];
|
|
32
|
+
return header ? `[${header}]` : match;
|
|
33
|
+
})
|
|
34
|
+
.replace(/\*/g, " × ")
|
|
35
|
+
.replace(/\//g, " ÷ ")
|
|
36
|
+
.replace(/\+/g, " + ")
|
|
37
|
+
.replace(/-/g, " - ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function describeSheet(filePath, sheetName, range) {
|
|
41
|
+
const workbook = XLSX.readFile(filePath, { cellFormula: true });
|
|
42
|
+
|
|
43
|
+
const sheet = workbook.Sheets[sheetName];
|
|
44
|
+
if (!sheet) {
|
|
45
|
+
const available = workbook.SheetNames.join(", ");
|
|
46
|
+
return `[Sheet "${sheetName}" not found in ${path.basename(filePath)}. Available: ${available}]`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const opts = { header: 1, defval: "" };
|
|
50
|
+
if (range) opts.range = range;
|
|
51
|
+
|
|
52
|
+
const rows = XLSX.utils.sheet_to_json(sheet, opts);
|
|
53
|
+
if (rows.length === 0) return `[No data found in ${sheetName}${range ? ` (${range})` : ""}]`;
|
|
54
|
+
|
|
55
|
+
const headers = rows[0];
|
|
56
|
+
const dataRows = rows.slice(1).filter(r => r.some(cell => cell !== ""));
|
|
57
|
+
const rowCount = dataRows.length;
|
|
58
|
+
|
|
59
|
+
const decodedRange = range ? XLSX.utils.decode_range(range) : XLSX.utils.decode_range(sheet["!ref"]);
|
|
60
|
+
const rangeColStart = decodedRange.s.c;
|
|
61
|
+
const rangeRowStart = decodedRange.s.r;
|
|
62
|
+
|
|
63
|
+
const formulaMap = {};
|
|
64
|
+
for (let R = decodedRange.s.r; R <= decodedRange.e.r; R++) {
|
|
65
|
+
for (let C = decodedRange.s.c; C <= decodedRange.e.c; C++) {
|
|
66
|
+
const cell = sheet[XLSX.utils.encode_cell({ r: R, c: C })];
|
|
67
|
+
if (cell && cell.f) {
|
|
68
|
+
const relRow = R - rangeRowStart;
|
|
69
|
+
const relCol = C - rangeColStart;
|
|
70
|
+
if (!formulaMap[relRow]) formulaMap[relRow] = {};
|
|
71
|
+
formulaMap[relRow][relCol] = cell.f;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const colFormulas = {};
|
|
77
|
+
Object.entries(formulaMap).forEach(([relRow, cols]) => {
|
|
78
|
+
if (Number(relRow) === 0) return;
|
|
79
|
+
Object.entries(cols).forEach(([relCol, formula]) => {
|
|
80
|
+
const humanised = humanizeFormula(formula, headers, rangeColStart);
|
|
81
|
+
const generalised = humanised.replace(/\b([A-Z]+)(\d+)\b/g, "$1n");
|
|
82
|
+
if (!colFormulas[relCol]) colFormulas[relCol] = new Set();
|
|
83
|
+
colFormulas[relCol].add(generalised);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const calcLogic = [];
|
|
88
|
+
Object.entries(colFormulas).forEach(([relCol, formulas]) => {
|
|
89
|
+
const header = headers[Number(relCol)] || `Col ${Number(relCol) + 1}`;
|
|
90
|
+
formulas.forEach(f => {
|
|
91
|
+
calcLogic.push(` ${header} = ${f}`);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const columns = headers.map((header, colIndex) => {
|
|
96
|
+
const values = dataRows.map(row => row[colIndex]).filter(v => v !== "" && v != null);
|
|
97
|
+
const numeric = values.filter(v => typeof v === "number");
|
|
98
|
+
const isNumeric = numeric.length > 0 && numeric.length > values.length * 0.8;
|
|
99
|
+
if (isNumeric) {
|
|
100
|
+
const min = Math.min(...numeric);
|
|
101
|
+
const max = Math.max(...numeric);
|
|
102
|
+
return `${header} (number, ${min}–${max})`;
|
|
103
|
+
}
|
|
104
|
+
const unique = [...new Set(values.map(String))].slice(0, 3);
|
|
105
|
+
return `${header} (text, e.g. ${unique.join(", ")})`;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const allRows = dataRows.map(row =>
|
|
109
|
+
headers.map((_, i) => row[i] ?? "").join(" | ")
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const lines = [
|
|
113
|
+
`[Excel: ${path.basename(filePath)} | Sheet: ${sheetName}${range ? ` | Range: ${range}` : ""} | ${rowCount} rows × ${headers.length} columns]`,
|
|
114
|
+
`Columns: ${columns.join("; ")}`,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (calcLogic.length > 0) {
|
|
118
|
+
lines.push(`Calculation logic:`);
|
|
119
|
+
lines.push(...calcLogic);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push(`Rows:`);
|
|
123
|
+
lines.push(...allRows.map(r => ` ${r}`));
|
|
124
|
+
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { resolveExcelPath, describeSheet };
|