Skip to content

Commit fb42540

Browse files
committed
Refactor and enhance OSF parser and documentation
## Summary - Improved string parsing in the OSF parser to handle escape sequences, including backslashes, quotes, and control characters. - Added a new function `escapeString` to ensure proper serialization of strings with escape sequences. - Updated tests to cover various scenarios for escape sequences in strings, bullet points, arrays, and objects. - Enhanced documentation to clarify the handling of escape sequences and updated the roadmap in the specification. ## Testing - Run `pnpm test` to verify all new and existing tests pass successfully.
1 parent 7644726 commit fb42540

File tree

10 files changed

+413
-99
lines changed

10 files changed

+413
-99
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ Join us on
191191
[GitHub Discussions](https://github.com/OmniScriptOSF/omniscript-core/discussions)
192192
to propose ideas, ask questions, or share feedback.
193193

194-
195194
## 💡 Vision
196195

197196
OmniScript Format (OSF) aims to be the universal document source language — a
@@ -200,6 +199,9 @@ world of AI collaboration and versioned knowledge.
200199

201200
## ❗ Known Limitations (v0.5)
202201

203-
- The initial CLI only covers parsing, linting and basic rendering; advanced conversion targets are under development.
204-
- Diagram blocks, citation syntax and macro support are planned for future revisions.
205-
- Formal grammar and normative references are scheduled for a later appendix release.
202+
- The initial CLI only covers parsing, linting and basic rendering; advanced
203+
conversion targets are under development.
204+
- Diagram blocks, citation syntax and macro support are planned for future
205+
revisions.
206+
- Formal grammar and normative references are scheduled for a later appendix
207+
release.

cli/src/osf.ts

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ const commands: CliCommand[] = [
3232
{
3333
name: 'render',
3434
description: 'Render OSF to various output formats',
35-
usage:
36-
'osf render <file> [--format <html|pdf|docx|pptx|xlsx>] [--output <file>]',
35+
usage: 'osf render <file> [--format <html|pdf|docx|pptx|xlsx>] [--output <file>]',
3736
args: ['file'],
3837
},
3938
{
@@ -149,10 +148,10 @@ class FormulaEvaluator {
149148

150149
// Replace cell references with actual values
151150
const cellRefRegex = /\b([A-Z]+\d+)\b/g;
152-
const processedExpr = expr.replace(cellRefRegex, (match) => {
151+
const processedExpr = expr.replace(cellRefRegex, match => {
153152
const [row, col] = this.cellRefToCoords(match);
154153
const value = this.getCellValue(row, col);
155-
154+
156155
// Convert to number if possible, otherwise use as string
157156
if (typeof value === 'number') {
158157
return value.toString();
@@ -181,14 +180,14 @@ class FormulaEvaluator {
181180
while (expr.includes('(')) {
182181
const innerMatch = expr.match(/\(([^()]+)\)/);
183182
if (!innerMatch) break;
184-
183+
185184
const innerResult = this.evaluateExpression(innerMatch[1]!);
186185
expr = expr.replace(innerMatch[0]!, innerResult.toString());
187186
}
188187

189188
// Handle multiplication and division (left to right, same precedence)
190189
expr = this.evaluateOperatorsLeftToRight(expr, ['*', '/']);
191-
190+
192191
// Handle addition and subtraction (left to right, same precedence)
193192
expr = this.evaluateOperatorsLeftToRight(expr, ['+', '-']);
194193

@@ -197,7 +196,7 @@ class FormulaEvaluator {
197196
if (isNaN(result)) {
198197
throw new Error(`Invalid expression: ${expr}`);
199198
}
200-
199+
201200
return result;
202201
}
203202

@@ -206,37 +205,44 @@ class FormulaEvaluator {
206205
// Create a regex that matches any of the operators
207206
const opPattern = operators.map(op => `\\${op}`).join('|');
208207
const regex = new RegExp(`(-?\\d+(?:\\.\\d+)?)(${opPattern})(-?\\d+(?:\\.\\d+)?)`, 'g');
209-
208+
210209
// Keep evaluating until no more matches
211210
while (regex.test(expr)) {
212211
regex.lastIndex = 0; // Reset regex
213212
expr = expr.replace(regex, (_, left, op, right) => {
214213
const leftNum = parseFloat(left);
215214
const rightNum = parseFloat(right);
216215
let result: number;
217-
216+
218217
switch (op) {
219-
case '+': result = leftNum + rightNum; break;
220-
case '-': result = leftNum - rightNum; break;
221-
case '*': result = leftNum * rightNum; break;
222-
case '/':
218+
case '+':
219+
result = leftNum + rightNum;
220+
break;
221+
case '-':
222+
result = leftNum - rightNum;
223+
break;
224+
case '*':
225+
result = leftNum * rightNum;
226+
break;
227+
case '/':
223228
if (rightNum === 0) throw new Error('Division by zero');
224-
result = leftNum / rightNum;
229+
result = leftNum / rightNum;
225230
break;
226-
default: throw new Error(`Unknown operator: ${op}`);
231+
default:
232+
throw new Error(`Unknown operator: ${op}`);
227233
}
228-
234+
229235
return result.toString();
230236
});
231237
}
232-
238+
233239
return expr;
234240
}
235241

236242
// Get all computed values for a sheet
237243
getAllComputedValues(maxRow: number, maxCol: number): Record<string, any> {
238244
const result: Record<string, any> = {};
239-
245+
240246
for (let r = 1; r <= maxRow; r++) {
241247
for (let c = 1; c <= maxCol; c++) {
242248
const key = `${r},${c}`;
@@ -251,7 +257,7 @@ class FormulaEvaluator {
251257
}
252258
}
253259
}
254-
260+
255261
return result;
256262
}
257263
}
@@ -402,15 +408,15 @@ function renderHtml(doc: OSFDocument): string {
402408
if (sheet.data) {
403409
// Evaluate formulas
404410
const evaluator = new FormulaEvaluator(sheet.data, sheet.formulas || []);
405-
411+
406412
// Calculate dimensions including formula cells
407413
const dataCoords = Object.keys(sheet.data).map(k => k.split(',').map(Number));
408414
const formulaCoords = (sheet.formulas || []).map((f: any) => f.cell);
409415
const allCoords = [...dataCoords, ...formulaCoords];
410-
416+
411417
const maxRow = Math.max(...allCoords.map(c => c[0]!));
412418
const maxCol = Math.max(...allCoords.map(c => c[1]!));
413-
419+
414420
// Get all computed values including formulas
415421
const allValues = evaluator.getAllComputedValues(maxRow, maxCol);
416422

@@ -420,12 +426,14 @@ function renderHtml(doc: OSFDocument): string {
420426
for (let c = 1; c <= maxCol; c++) {
421427
const key = `${r},${c}`;
422428
const val = allValues[key] ?? '';
423-
const hasFormula = sheet.formulas?.some((f: any) => f.cell[0] === r && f.cell[1] === c);
429+
const hasFormula = sheet.formulas?.some(
430+
(f: any) => f.cell[0] === r && f.cell[1] === c
431+
);
424432
const isError = typeof val === 'string' && val.startsWith('#ERROR:');
425-
426-
const cssClass = isError ? 'error' : (hasFormula ? 'computed' : '');
433+
434+
const cssClass = isError ? 'error' : hasFormula ? 'computed' : '';
427435
const cellContent = isError ? val.replace('#ERROR: ', '') : val;
428-
436+
429437
parts.push(` <td class="${cssClass}">${cellContent}</td>`);
430438
}
431439
parts.push(' </tr>');
@@ -525,15 +533,15 @@ function exportMarkdown(doc: OSFDocument): string {
525533
if (sheet.data) {
526534
// Evaluate formulas
527535
const evaluator = new FormulaEvaluator(sheet.data, sheet.formulas || []);
528-
536+
529537
// Calculate dimensions including formula cells
530538
const dataCoords = Object.keys(sheet.data).map(k => k.split(',').map(Number));
531539
const formulaCoords = (sheet.formulas || []).map((f: any) => f.cell);
532540
const allCoords = [...dataCoords, ...formulaCoords];
533-
541+
534542
const maxRow = Math.max(...allCoords.map(c => c[0]!));
535543
const maxCol = Math.max(...allCoords.map(c => c[1]!));
536-
544+
537545
// Get all computed values including formulas
538546
const allValues = evaluator.getAllComputedValues(maxRow, maxCol);
539547

@@ -542,8 +550,10 @@ function exportMarkdown(doc: OSFDocument): string {
542550
for (let c = 1; c <= maxCol; c++) {
543551
const key = `${r},${c}`;
544552
const val = allValues[key] ?? '';
545-
const hasFormula = sheet.formulas?.some((f: any) => f.cell[0] === r && f.cell[1] === c);
546-
553+
const hasFormula = sheet.formulas?.some(
554+
(f: any) => f.cell[0] === r && f.cell[1] === c
555+
);
556+
547557
if (hasFormula && typeof val === 'number') {
548558
// Show computed value with indication it's calculated
549559
cells.push(`${val} *(calc)*`);
@@ -583,35 +593,35 @@ function exportJson(doc: OSFDocument): string {
583593
case 'sheet': {
584594
const sheet: any = { ...block };
585595
delete sheet.type;
586-
596+
587597
if (sheet.data) {
588598
// Evaluate formulas and include computed values
589599
const evaluator = new FormulaEvaluator(sheet.data, sheet.formulas || []);
590-
600+
591601
// Calculate dimensions including formula cells
592602
const dataCoords = Object.keys(sheet.data).map((k: string) => k.split(',').map(Number));
593603
const formulaCoords = (sheet.formulas || []).map((f: any) => f.cell);
594604
const allCoords = [...dataCoords, ...formulaCoords];
595-
605+
596606
const maxRow = Math.max(...allCoords.map(c => c[0]!));
597607
const maxCol = Math.max(...allCoords.map(c => c[1]!));
598-
608+
599609
// Get all computed values including formulas
600610
const allValues = evaluator.getAllComputedValues(maxRow, maxCol);
601-
611+
602612
// Convert to array format with computed values
603613
sheet.data = Object.entries(allValues).map(([cell, value]) => {
604614
const [r, c] = cell.split(',').map(Number);
605615
const hasFormula = sheet.formulas?.some((f: any) => f.cell[0] === r && f.cell[1] === c);
606-
return {
607-
row: r,
608-
col: c,
616+
return {
617+
row: r,
618+
col: c,
609619
value,
610-
computed: hasFormula
620+
computed: hasFormula,
611621
};
612622
});
613623
}
614-
624+
615625
if (sheet.formulas) {
616626
sheet.formulas = sheet.formulas.map((f: any) => ({
617627
row: f.cell[0],
@@ -763,9 +773,7 @@ function main(): void {
763773
output = renderXlsx(doc);
764774
break;
765775
default:
766-
throw new Error(
767-
`Unknown format: ${format}. Supported: html, pdf, docx, pptx, xlsx`
768-
);
776+
throw new Error(`Unknown format: ${format}. Supported: html, pdf, docx, pptx, xlsx`);
769777
}
770778

771779
if (outputFile) {

cli/tests/cli.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,16 +308,16 @@ describe('OSF CLI', () => {
308308
expect(exported.docs).toHaveLength(1);
309309
expect(exported.slides).toHaveLength(1);
310310
expect(exported.sheets).toHaveLength(1);
311-
311+
312312
// Check that computed values are included
313313
const sheet = exported.sheets[0];
314314
expect(sheet.data).toBeDefined();
315315
expect(Array.isArray(sheet.data)).toBe(true);
316-
316+
317317
// Find computed cells
318318
const computedCells = sheet.data.filter((cell: any) => cell.computed === true);
319319
expect(computedCells.length).toBeGreaterThan(0);
320-
320+
321321
// Check specific computed values
322322
const growthCell1 = sheet.data.find((cell: any) => cell.row === 2 && cell.col === 3);
323323
const growthCell2 = sheet.data.find((cell: any) => cell.row === 3 && cell.col === 3);
@@ -336,18 +336,18 @@ describe('OSF CLI', () => {
336336

337337
const exported = JSON.parse(result);
338338
const sheet = exported.sheets[0];
339-
339+
340340
// Check computed values
341341
const cellC1 = sheet.data.find((cell: any) => cell.row === 1 && cell.col === 3);
342342
const cellD1 = sheet.data.find((cell: any) => cell.row === 1 && cell.col === 4);
343343
const cellC2 = sheet.data.find((cell: any) => cell.row === 2 && cell.col === 3);
344344
const cellD2 = sheet.data.find((cell: any) => cell.row === 2 && cell.col === 4);
345-
345+
346346
expect(cellC1?.value).toBe(30); // A1+B1 = 10+20 = 30
347347
expect(cellD1?.value).toBe(200); // A1*B1 = 10*20 = 200
348348
expect(cellC2?.value).toBe(20); // A2+B2 = 5+15 = 20
349349
expect(cellD2?.value).toBe(50); // C1+C2 = 30+20 = 50
350-
350+
351351
// Check computed flags
352352
expect(cellC1?.computed).toBe(true);
353353
expect(cellD1?.computed).toBe(true);

docs/architecture.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ This repository hosts the reference implementation of **OmniScript Format
44
(OSF)**. The codebase is organised as a monorepo with separate packages for the
55
parser and CLI.
66

7-
The `spec/` directory contains versioned specifications. The current release is [spec/v0.5](../spec/v0.5/) which defines the grammar and JSON schema used by the parser.
7+
The `spec/` directory contains versioned specifications. The current release is
8+
[spec/v0.5](../spec/v0.5/) which defines the grammar and JSON schema used by the
9+
parser.
810

911
```
1012
omniscript-core/

docs/contributing.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
We welcome pull requests!
44

5-
1. Read the OSF v0.5 specification in [spec/v0.5](../spec/v0.5/) to understand the current language features.
5+
1. Read the OSF v0.5 specification in [spec/v0.5](../spec/v0.5/) to understand
6+
the current language features.
67
2. Follow the coding style used in the parser and CLI packages.
78
3. Tests are located under `tests/` and `parser/tests`. Please add tests for new
89
functionality.

docs/spec-v0.5-overview.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# OSF v0.5 Overview
22

3-
This document summarises the key elements of the OmniScript Format v0.5 specification.
4-
Refer to the files under `spec/v0.5/` for the authoritative specification.
3+
This document summarises the key elements of the OmniScript Format v0.5
4+
specification. Refer to the files under `spec/v0.5/` for the authoritative
5+
specification.
56

67
## Core Blocks
78

0 commit comments

Comments
 (0)