kicadts 0.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.
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/.vscode/settings.json +16 -0
- package/AGENTS.md +30 -0
- package/README.md +206 -0
- package/biome.json +93 -0
- package/bun.lock +48 -0
- package/bunfig.toml +5 -0
- package/lib/index.ts +1 -0
- package/lib/sexpr/base-classes/SxClass.ts +164 -0
- package/lib/sexpr/base-classes/SxPrimitiveBoolean.ts +35 -0
- package/lib/sexpr/base-classes/SxPrimitiveNumber.ts +26 -0
- package/lib/sexpr/base-classes/SxPrimitiveString.ts +26 -0
- package/lib/sexpr/classes/At.ts +38 -0
- package/lib/sexpr/classes/Bus.ts +83 -0
- package/lib/sexpr/classes/BusEntry.ts +142 -0
- package/lib/sexpr/classes/Color.ts +29 -0
- package/lib/sexpr/classes/Dnp.ts +8 -0
- package/lib/sexpr/classes/EmbeddedFonts.ts +70 -0
- package/lib/sexpr/classes/ExcludeFromSim.ts +8 -0
- package/lib/sexpr/classes/FieldsAutoplaced.ts +8 -0
- package/lib/sexpr/classes/Footprint.ts +719 -0
- package/lib/sexpr/classes/FootprintAttr.ts +102 -0
- package/lib/sexpr/classes/FootprintAutoplaceCost180.ts +9 -0
- package/lib/sexpr/classes/FootprintAutoplaceCost90.ts +9 -0
- package/lib/sexpr/classes/FootprintClearance.ts +9 -0
- package/lib/sexpr/classes/FootprintDescr.ts +44 -0
- package/lib/sexpr/classes/FootprintLocked.ts +32 -0
- package/lib/sexpr/classes/FootprintModel.ts +145 -0
- package/lib/sexpr/classes/FootprintNetTiePadGroups.ts +50 -0
- package/lib/sexpr/classes/FootprintPad.ts +705 -0
- package/lib/sexpr/classes/FootprintPath.ts +44 -0
- package/lib/sexpr/classes/FootprintPlaced.ts +32 -0
- package/lib/sexpr/classes/FootprintPrivateLayers.ts +56 -0
- package/lib/sexpr/classes/FootprintSheetfile.ts +44 -0
- package/lib/sexpr/classes/FootprintSheetname.ts +44 -0
- package/lib/sexpr/classes/FootprintSolderMaskMargin.ts +9 -0
- package/lib/sexpr/classes/FootprintSolderPasteMargin.ts +9 -0
- package/lib/sexpr/classes/FootprintSolderPasteRatio.ts +9 -0
- package/lib/sexpr/classes/FootprintTags.ts +44 -0
- package/lib/sexpr/classes/FootprintTedit.ts +21 -0
- package/lib/sexpr/classes/FootprintThermalGap.ts +9 -0
- package/lib/sexpr/classes/FootprintThermalWidth.ts +9 -0
- package/lib/sexpr/classes/FootprintZoneConnect.ts +9 -0
- package/lib/sexpr/classes/FpArc.ts +289 -0
- package/lib/sexpr/classes/FpCircle.ts +293 -0
- package/lib/sexpr/classes/FpLine.ts +288 -0
- package/lib/sexpr/classes/FpPoly.ts +266 -0
- package/lib/sexpr/classes/FpPolyFill.ts +48 -0
- package/lib/sexpr/classes/FpPolyLocked.ts +40 -0
- package/lib/sexpr/classes/FpRect.ts +293 -0
- package/lib/sexpr/classes/FpText.ts +341 -0
- package/lib/sexpr/classes/FpTextBox.ts +412 -0
- package/lib/sexpr/classes/GrLine.ts +245 -0
- package/lib/sexpr/classes/GrLineAngle.ts +32 -0
- package/lib/sexpr/classes/GrLineEnd.ts +61 -0
- package/lib/sexpr/classes/GrLineLocked.ts +40 -0
- package/lib/sexpr/classes/GrLineStart.ts +61 -0
- package/lib/sexpr/classes/GrText.ts +202 -0
- package/lib/sexpr/classes/Image.ts +256 -0
- package/lib/sexpr/classes/InBom.ts +8 -0
- package/lib/sexpr/classes/Junction.ts +134 -0
- package/lib/sexpr/classes/KicadPcb.ts +313 -0
- package/lib/sexpr/classes/KicadSch.ts +303 -0
- package/lib/sexpr/classes/KicadSchGenerator.ts +32 -0
- package/lib/sexpr/classes/KicadSchGeneratorVersion.ts +30 -0
- package/lib/sexpr/classes/KicadSchVersion.ts +22 -0
- package/lib/sexpr/classes/Label.ts +136 -0
- package/lib/sexpr/classes/Layer.ts +51 -0
- package/lib/sexpr/classes/Layers.ts +47 -0
- package/lib/sexpr/classes/LibSymbols.ts +61 -0
- package/lib/sexpr/classes/NoConnect.ts +73 -0
- package/lib/sexpr/classes/OnBoard.ts +8 -0
- package/lib/sexpr/classes/PadChamfer.ts +50 -0
- package/lib/sexpr/classes/PadChamferRatio.ts +9 -0
- package/lib/sexpr/classes/PadClearance.ts +9 -0
- package/lib/sexpr/classes/PadDieLength.ts +9 -0
- package/lib/sexpr/classes/PadDrill.ts +145 -0
- package/lib/sexpr/classes/PadDrillOffset.ts +54 -0
- package/lib/sexpr/classes/PadLayers.ts +59 -0
- package/lib/sexpr/classes/PadNet.ts +56 -0
- package/lib/sexpr/classes/PadOptions.ts +182 -0
- package/lib/sexpr/classes/PadPinFunction.ts +9 -0
- package/lib/sexpr/classes/PadPinType.ts +9 -0
- package/lib/sexpr/classes/PadPrimitiveGrArc.ts +254 -0
- package/lib/sexpr/classes/PadPrimitiveGrCircle.ts +279 -0
- package/lib/sexpr/classes/PadPrimitiveGrLine.ts +126 -0
- package/lib/sexpr/classes/PadPrimitives.ts +289 -0
- package/lib/sexpr/classes/PadRectDelta.ts +57 -0
- package/lib/sexpr/classes/PadRoundrectRratio.ts +9 -0
- package/lib/sexpr/classes/PadSize.ts +54 -0
- package/lib/sexpr/classes/PadSolderMaskMargin.ts +9 -0
- package/lib/sexpr/classes/PadSolderPasteMargin.ts +9 -0
- package/lib/sexpr/classes/PadSolderPasteMarginRatio.ts +9 -0
- package/lib/sexpr/classes/PadTeardrops.ts +208 -0
- package/lib/sexpr/classes/PadThermalBridgeAngle.ts +9 -0
- package/lib/sexpr/classes/PadThermalGap.ts +9 -0
- package/lib/sexpr/classes/PadThermalWidth.ts +9 -0
- package/lib/sexpr/classes/PadZoneConnect.ts +9 -0
- package/lib/sexpr/classes/Paper.ts +119 -0
- package/lib/sexpr/classes/PcbGeneral.ts +75 -0
- package/lib/sexpr/classes/PcbGeneralLegacyTeardrops.ts +44 -0
- package/lib/sexpr/classes/PcbGeneralThickness.ts +9 -0
- package/lib/sexpr/classes/PcbGenerator.ts +16 -0
- package/lib/sexpr/classes/PcbGeneratorVersion.ts +16 -0
- package/lib/sexpr/classes/PcbLayerDefinition.ts +102 -0
- package/lib/sexpr/classes/PcbLayers.ts +34 -0
- package/lib/sexpr/classes/PcbNet.ts +56 -0
- package/lib/sexpr/classes/PcbVersion.ts +9 -0
- package/lib/sexpr/classes/Property.ts +246 -0
- package/lib/sexpr/classes/PropertyHide.ts +9 -0
- package/lib/sexpr/classes/PropertyUnlocked.ts +9 -0
- package/lib/sexpr/classes/Pts.ts +65 -0
- package/lib/sexpr/classes/RenderCache.ts +221 -0
- package/lib/sexpr/classes/SchematicText.ts +141 -0
- package/lib/sexpr/classes/Segment.ts +222 -0
- package/lib/sexpr/classes/SegmentEnd.ts +59 -0
- package/lib/sexpr/classes/SegmentLocked.ts +33 -0
- package/lib/sexpr/classes/SegmentNet.ts +62 -0
- package/lib/sexpr/classes/SegmentStart.ts +59 -0
- package/lib/sexpr/classes/Setup/PcbPlotParams.ts +729 -0
- package/lib/sexpr/classes/Setup/PcbPlotParamsBase.ts +9 -0
- package/lib/sexpr/classes/Setup/PcbPlotParamsNumericProperties.ts +105 -0
- package/lib/sexpr/classes/Setup/PcbPlotParamsStringPropertiesA.ts +104 -0
- package/lib/sexpr/classes/Setup/PcbPlotParamsStringPropertiesB.ts +105 -0
- package/lib/sexpr/classes/Setup/Setup.ts +573 -0
- package/lib/sexpr/classes/Setup/SetupPropertyTypes.ts +119 -0
- package/lib/sexpr/classes/Setup/Stackup.ts +140 -0
- package/lib/sexpr/classes/Setup/StackupLayer.ts +233 -0
- package/lib/sexpr/classes/Setup/StackupLayerProperties.ts +78 -0
- package/lib/sexpr/classes/Setup/StackupProperties.ts +41 -0
- package/lib/sexpr/classes/Setup/base.ts +167 -0
- package/lib/sexpr/classes/Setup/index.ts +14 -0
- package/lib/sexpr/classes/Setup/setupMultiValueProperties.ts +54 -0
- package/lib/sexpr/classes/Setup/setupNumericProperties.ts +151 -0
- package/lib/sexpr/classes/Setup/setupPropertyHandlers.ts +90 -0
- package/lib/sexpr/classes/Setup/setupStringProperties.ts +75 -0
- package/lib/sexpr/classes/Sheet.ts +205 -0
- package/lib/sexpr/classes/SheetFill.ts +44 -0
- package/lib/sexpr/classes/SheetInstances.ts +168 -0
- package/lib/sexpr/classes/SheetInstancesRoot.ts +165 -0
- package/lib/sexpr/classes/SheetPin.ts +122 -0
- package/lib/sexpr/classes/SheetProperty.ts +115 -0
- package/lib/sexpr/classes/SheetSize.ts +44 -0
- package/lib/sexpr/classes/Stroke.ts +58 -0
- package/lib/sexpr/classes/StrokeType.ts +34 -0
- package/lib/sexpr/classes/Symbol.ts +1541 -0
- package/lib/sexpr/classes/TextEffects.ts +444 -0
- package/lib/sexpr/classes/TitleBlock.ts +352 -0
- package/lib/sexpr/classes/Unit.ts +28 -0
- package/lib/sexpr/classes/Uuid.ts +8 -0
- package/lib/sexpr/classes/Via.ts +328 -0
- package/lib/sexpr/classes/ViaNet.ts +59 -0
- package/lib/sexpr/classes/Width.ts +8 -0
- package/lib/sexpr/classes/Wire.ts +91 -0
- package/lib/sexpr/classes/Xy.ts +35 -0
- package/lib/sexpr/classes/Zone.ts +41 -0
- package/lib/sexpr/index.ts +130 -0
- package/lib/sexpr/parseKicadSexpr.ts +5 -0
- package/lib/sexpr/parseToPrimitiveSExpr.ts +240 -0
- package/lib/sexpr/utils/indentLines.ts +3 -0
- package/lib/sexpr/utils/parseYesNo.ts +12 -0
- package/lib/sexpr/utils/quoteSExprString.ts +8 -0
- package/lib/sexpr/utils/strokeFromArgs.ts +19 -0
- package/lib/sexpr/utils/toNumberValue.ts +13 -0
- package/lib/sexpr/utils/toStringValue.ts +10 -0
- package/package.json +26 -0
- package/scripts/download-references.ts +66 -0
- package/tests/fixtures/expectEqualPrimitiveSExpr.ts +200 -0
- package/tests/sexpr/KicadPcbDemos.test.ts +48 -0
- package/tests/sexpr/KicadSchDemos.test.ts +49 -0
- package/tests/sexpr/classes/Footprint.test.ts +277 -0
- package/tests/sexpr/classes/FootprintPad.test.ts +71 -0
- package/tests/sexpr/classes/FpArc.test.ts +45 -0
- package/tests/sexpr/classes/FpCircle.test.ts +39 -0
- package/tests/sexpr/classes/FpPoly.test.ts +43 -0
- package/tests/sexpr/classes/FpRect.test.ts +40 -0
- package/tests/sexpr/classes/FpTextBox.test.ts +84 -0
- package/tests/sexpr/classes/Image.test.ts +50 -0
- package/tests/sexpr/classes/KicadSch.test.ts +97 -0
- package/tests/sexpr/classes/Paper.test.ts +30 -0
- package/tests/sexpr/classes/Property.test.ts +48 -0
- package/tests/sexpr/classes/Setup.test.ts +189 -0
- package/tests/sexpr/classes/Sheet.test.ts +107 -0
- package/tests/sexpr/classes/Stroke.test.ts +15 -0
- package/tests/sexpr/classes/Symbol.test.ts +96 -0
- package/tests/sexpr/classes/TextEffects.test.ts +56 -0
- package/tests/sexpr/classes/TitleBlock.test.ts +40 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
+
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Default to using Bun instead of Node.js.
|
|
8
|
+
|
|
9
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
15
|
+
|
|
16
|
+
## APIs
|
|
17
|
+
|
|
18
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
19
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
20
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
21
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
22
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
23
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
24
|
+
- Bun.$`ls` instead of execa.
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
Use `bun test` to run tests.
|
|
29
|
+
|
|
30
|
+
```ts#index.test.ts
|
|
31
|
+
import { test, expect } from "bun:test";
|
|
32
|
+
|
|
33
|
+
test("hello world", () => {
|
|
34
|
+
expect(1).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Frontend
|
|
39
|
+
|
|
40
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
41
|
+
|
|
42
|
+
Server:
|
|
43
|
+
|
|
44
|
+
```ts#index.ts
|
|
45
|
+
import index from "./index.html"
|
|
46
|
+
|
|
47
|
+
Bun.serve({
|
|
48
|
+
routes: {
|
|
49
|
+
"/": index,
|
|
50
|
+
"/api/users/:id": {
|
|
51
|
+
GET: (req) => {
|
|
52
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// optional websocket support
|
|
57
|
+
websocket: {
|
|
58
|
+
open: (ws) => {
|
|
59
|
+
ws.send("Hello, world!");
|
|
60
|
+
},
|
|
61
|
+
message: (ws, message) => {
|
|
62
|
+
ws.send(message);
|
|
63
|
+
},
|
|
64
|
+
close: (ws) => {
|
|
65
|
+
// handle close
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
development: {
|
|
69
|
+
hmr: true,
|
|
70
|
+
console: true,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
76
|
+
|
|
77
|
+
```html#index.html
|
|
78
|
+
<html>
|
|
79
|
+
<body>
|
|
80
|
+
<h1>Hello, world!</h1>
|
|
81
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With the following `frontend.tsx`:
|
|
87
|
+
|
|
88
|
+
```tsx#frontend.tsx
|
|
89
|
+
import React from "react";
|
|
90
|
+
|
|
91
|
+
// import .css files directly and it works
|
|
92
|
+
import './index.css';
|
|
93
|
+
|
|
94
|
+
import { createRoot } from "react-dom/client";
|
|
95
|
+
|
|
96
|
+
const root = createRoot(document.body);
|
|
97
|
+
|
|
98
|
+
export default function Frontend() {
|
|
99
|
+
return <h1>Hello, world!</h1>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
root.render(<Frontend />);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then, run index.ts
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bun --hot ./index.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workbench.colorCustomizations": {
|
|
3
|
+
"commandCenter.border": "#15202b99",
|
|
4
|
+
"sash.hoverBorder": "#18f7e8",
|
|
5
|
+
"statusBar.background": "#07d5c7",
|
|
6
|
+
"statusBar.foreground": "#15202b",
|
|
7
|
+
"statusBarItem.hoverBackground": "#05a499",
|
|
8
|
+
"statusBarItem.remoteBackground": "#07d5c7",
|
|
9
|
+
"statusBarItem.remoteForeground": "#15202b",
|
|
10
|
+
"titleBar.activeBackground": "#07d5c7",
|
|
11
|
+
"titleBar.activeForeground": "#15202b",
|
|
12
|
+
"titleBar.inactiveBackground": "#07d5c799",
|
|
13
|
+
"titleBar.inactiveForeground": "#15202b99"
|
|
14
|
+
},
|
|
15
|
+
"peacock.color": "#07d5c7"
|
|
16
|
+
}
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
This is the `kicadts` typescript library. It's still in early developement and
|
|
2
|
+
we're trying to make sure we parse the entire KiCad S-expression specification.
|
|
3
|
+
|
|
4
|
+
You can find the difference specifications in the `references` directory, the
|
|
5
|
+
most extensive one is `references/SEXPR_MAIN.adoc` (use `bun run scripts/download-references.ts` to download if it's not already there)
|
|
6
|
+
|
|
7
|
+
- `references/SCHEMATIC_SEXPR.adoc`
|
|
8
|
+
- `references/PCB_SEXPR.adoc`
|
|
9
|
+
- `references/FOOTPRINT_SEXPR.adoc`
|
|
10
|
+
- `references/SCH_SYM_SEXPR.adoc`
|
|
11
|
+
- `references/SEXPR_MAIN.adoc`
|
|
12
|
+
|
|
13
|
+
### Tips
|
|
14
|
+
|
|
15
|
+
- Every S-expression token must have a corresponding `SxClass` subclass and be registered via `SxClass.register`; missing registrations cause parse failures.
|
|
16
|
+
- Tokens reused under different parents (for example `type` under `stroke`) require `static parentToken` overrides so the correct class resolves during parsing.
|
|
17
|
+
- When a class accepts unordered child tokens, set `_propertyMap` using `loadProperties` to make getters convenient and keep `getString()` deterministic.
|
|
18
|
+
- Snapshot tests with `bun test` provide quick verification that `getString()` matches the KiCad formatting expectations.
|
|
19
|
+
|
|
20
|
+
### Major Refactor Notice
|
|
21
|
+
|
|
22
|
+
- The code in this repo is only partially migrated to the new pattern
|
|
23
|
+
- NEW PATTERN: Constructors never take `PrimitiveSExpr` arguments
|
|
24
|
+
- NEW PATTERN: All classes have a `fromSexprPrimitives` static method that takes a `PrimitiveSExpr` array and returns an instance of the class
|
|
25
|
+
- NEW PATTERN: Classes have ergonomic getters and setters for properties
|
|
26
|
+
- NEW PATTERN: Never has "extras" property, everything becomes an `SxClass`
|
|
27
|
+
- NEW PATTERN: Never have "switch cases" that switch on the token. Always use the `SxClass.parsePrimitiveSexpr` or `SxClass.parsePrimitivesToClassProperties` methods to parse arrays of `PrimitiveSExpr` into instances of the correct class
|
|
28
|
+
- NEW PATTERN: Never track the order of children unless absolutely necessary, use the `_sx*` properties and `getChildren` to return the children in a predefined order
|
|
29
|
+
- NEW PATTERN: Never track unknown children, throw an error/allow an error to be thrown if you encounter a child PrimitiveSExpr that can't be parsed. You can introduce new `_sx*` properties and a new class to prevent the error
|
|
30
|
+
- NEW PATTERN: One class per file
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# kicadts
|
|
2
|
+
|
|
3
|
+
`kicadts` is a TypeScript-first toolkit for reading, editing, and generating KiCad S-expression documents. Every KiCad token is modeled as a class, so you can compose schematics, boards, and footprints entirely in TypeScript and emit KiCad-compatible files with deterministic formatting.
|
|
4
|
+
|
|
5
|
+
## Local Setup
|
|
6
|
+
|
|
7
|
+
This repository uses [Bun](https://bun.sh) for scripts and testing.
|
|
8
|
+
|
|
9
|
+
- `bun install`
|
|
10
|
+
- `bun test` — optional, but handy to confirm we still round-trip the KiCad demo files
|
|
11
|
+
|
|
12
|
+
## Build KiCad Schematics
|
|
13
|
+
|
|
14
|
+
The high-level classes (`KicadSch`, `Sheet`, `SchematicSymbol`, `Wire`, …) expose setters and getters for their children. Populate the model, then call `getString()` to emit KiCad’s S-expression.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { promises as fs } from "node:fs"
|
|
18
|
+
import {
|
|
19
|
+
At,
|
|
20
|
+
KicadSch,
|
|
21
|
+
Paper,
|
|
22
|
+
Property,
|
|
23
|
+
Sheet,
|
|
24
|
+
SchematicSymbol,
|
|
25
|
+
TitleBlock,
|
|
26
|
+
Wire,
|
|
27
|
+
Pts,
|
|
28
|
+
Xy,
|
|
29
|
+
} from "kicadts"
|
|
30
|
+
|
|
31
|
+
const schematic = new KicadSch()
|
|
32
|
+
schematic.version = 20240101
|
|
33
|
+
schematic.generator = "kicadts-demo"
|
|
34
|
+
|
|
35
|
+
const title = new TitleBlock()
|
|
36
|
+
title.title = "Demo Schematic"
|
|
37
|
+
title.company = "Example Labs"
|
|
38
|
+
schematic.titleBlock = title
|
|
39
|
+
|
|
40
|
+
const paper = new Paper()
|
|
41
|
+
paper.size = "A4"
|
|
42
|
+
schematic.paper = paper
|
|
43
|
+
schematic.properties = [new Property("Sheetfile", "demo.kicad_sch")]
|
|
44
|
+
|
|
45
|
+
const sheet = new Sheet()
|
|
46
|
+
sheet.position = new At([0, 0, 0])
|
|
47
|
+
sheet.size = { width: 100, height: 80 }
|
|
48
|
+
schematic.sheets = [sheet]
|
|
49
|
+
|
|
50
|
+
const symbol = new SchematicSymbol()
|
|
51
|
+
symbol.libraryId = "Device:R"
|
|
52
|
+
symbol.at = new At([25.4, 12.7])
|
|
53
|
+
schematic.symbols = [symbol]
|
|
54
|
+
|
|
55
|
+
const wire = new Wire()
|
|
56
|
+
wire.points = new Pts([new Xy(0, 0), new Xy(25.4, 12.7)])
|
|
57
|
+
schematic.wires = [wire]
|
|
58
|
+
|
|
59
|
+
await fs.writeFile("demo.kicad_sch", schematic.getString())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Build KiCad PCBs
|
|
63
|
+
|
|
64
|
+
Boards follow the same pattern. Compose `KicadPcb` with nets, footprints, segments, and zones. Footprints are reusable whether you embed them on a board or export them as `.kicad_mod` files.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { promises as fs } from "node:fs"
|
|
68
|
+
import {
|
|
69
|
+
At,
|
|
70
|
+
Footprint,
|
|
71
|
+
FootprintPad,
|
|
72
|
+
FpText,
|
|
73
|
+
KicadPcb,
|
|
74
|
+
PadLayers,
|
|
75
|
+
PadNet,
|
|
76
|
+
PadSize,
|
|
77
|
+
PcbNet,
|
|
78
|
+
TextEffects,
|
|
79
|
+
} from "kicadts"
|
|
80
|
+
|
|
81
|
+
const pcb = new KicadPcb()
|
|
82
|
+
pcb.version = 20240101
|
|
83
|
+
pcb.generator = "kicadts-demo"
|
|
84
|
+
|
|
85
|
+
const netGnd = new PcbNet(1, "GND")
|
|
86
|
+
const netSignal = new PcbNet(2, "Net-(R1-Pad2)")
|
|
87
|
+
pcb.nets = [netGnd, netSignal]
|
|
88
|
+
|
|
89
|
+
const footprint = new Footprint()
|
|
90
|
+
footprint.libraryLink = "Resistor_SMD:R_0603"
|
|
91
|
+
footprint.layer = "F.Cu"
|
|
92
|
+
footprint.position = new At([10, 5, 90])
|
|
93
|
+
|
|
94
|
+
const makeText = (type: string, text: string, x: number, y: number, layer: string) => {
|
|
95
|
+
const fpText = new FpText()
|
|
96
|
+
fpText.type = type
|
|
97
|
+
fpText.text = text
|
|
98
|
+
fpText.position = new At([x, y])
|
|
99
|
+
fpText.layer = layer
|
|
100
|
+
const effects = new TextEffects()
|
|
101
|
+
effects.font.size = { height: 1, width: 1 }
|
|
102
|
+
effects.font.thickness = 0.15
|
|
103
|
+
fpText.effects = effects
|
|
104
|
+
return fpText
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
footprint.fpTexts = [
|
|
108
|
+
makeText("reference", "R1", 0, -1.5, "F.SilkS"),
|
|
109
|
+
makeText("value", "10k", 0, 1.5, "F.Fab"),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const pad = (number: string, x: number, net: PcbNet) => {
|
|
113
|
+
const fpPad = new FootprintPad(number, "smd", "roundrect")
|
|
114
|
+
fpPad.at = new At([x, 0])
|
|
115
|
+
fpPad.size = new PadSize(1.05, 0.95)
|
|
116
|
+
fpPad.layers = new PadLayers(["F.Cu", "F.Paste", "F.Mask"])
|
|
117
|
+
fpPad.roundrectRatio = 0.25
|
|
118
|
+
fpPad.net = new PadNet(net.id, net.name)
|
|
119
|
+
fpPad.pinfunction = number
|
|
120
|
+
fpPad.pintype = "passive"
|
|
121
|
+
return fpPad
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
footprint.fpPads = [pad("1", -0.8, netGnd), pad("2", 0.8, netSignal)]
|
|
125
|
+
pcb.footprints = [footprint]
|
|
126
|
+
|
|
127
|
+
await fs.writeFile("demo.kicad_pcb", pcb.getString())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Build Stand-Alone Footprints
|
|
131
|
+
|
|
132
|
+
Footprints can live outside a board file (for library `.kicad_mod` entries). Populate `Footprint` and write the S-expression to disk.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { promises as fs } from "node:fs"
|
|
136
|
+
import {
|
|
137
|
+
At,
|
|
138
|
+
Footprint,
|
|
139
|
+
FootprintPad,
|
|
140
|
+
FpText,
|
|
141
|
+
PadLayers,
|
|
142
|
+
PadSize,
|
|
143
|
+
TextEffects,
|
|
144
|
+
} from "kicadts"
|
|
145
|
+
|
|
146
|
+
const footprint = new Footprint()
|
|
147
|
+
footprint.libraryLink = "Demo:TestPad"
|
|
148
|
+
footprint.layer = "F.Cu"
|
|
149
|
+
footprint.position = new At([0, 0])
|
|
150
|
+
|
|
151
|
+
const label = new FpText()
|
|
152
|
+
label.type = "reference"
|
|
153
|
+
label.text = "REF**"
|
|
154
|
+
label.position = new At([0, -1.5])
|
|
155
|
+
label.layer = "F.SilkS"
|
|
156
|
+
const effects = new TextEffects()
|
|
157
|
+
effects.font.size = { height: 1, width: 1 }
|
|
158
|
+
effects.font.thickness = 0.15
|
|
159
|
+
label.effects = effects
|
|
160
|
+
|
|
161
|
+
footprint.fpTexts = [label]
|
|
162
|
+
|
|
163
|
+
const pad = new FootprintPad("1", "smd", "rect")
|
|
164
|
+
pad.at = new At([0, 0])
|
|
165
|
+
pad.size = new PadSize(1.5, 1.5)
|
|
166
|
+
pad.layers = new PadLayers(["F.Cu", "F.Paste", "F.Mask"])
|
|
167
|
+
footprint.fpPads = [pad]
|
|
168
|
+
|
|
169
|
+
await fs.writeFile("Demo_TestPad.kicad_mod", footprint.getString())
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Load Existing KiCad Files
|
|
173
|
+
|
|
174
|
+
Parsing works for schematics, boards, footprints, or any KiCad S-expression. `parseKicadSexpr` returns an array of `SxClass` instances; narrow to the concrete class with `instanceof` and mutate as needed.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { promises as fs } from "node:fs"
|
|
178
|
+
import { KicadPcb, KicadSch, Footprint, parseKicadSexpr } from "kicadts"
|
|
179
|
+
|
|
180
|
+
const load = async (path: string) => {
|
|
181
|
+
const raw = await fs.readFile(path, "utf8")
|
|
182
|
+
const [root] = parseKicadSexpr(raw)
|
|
183
|
+
|
|
184
|
+
if (root instanceof KicadSch) {
|
|
185
|
+
root.generator = "kicadts"
|
|
186
|
+
return root
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (root instanceof KicadPcb) {
|
|
190
|
+
root.generatorVersion = "(generated programmatically)"
|
|
191
|
+
return root
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (root instanceof Footprint) {
|
|
195
|
+
root.descr = "Imported with kicadts"
|
|
196
|
+
return root
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new Error(`Unsupported root token: ${root.token}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const updated = await load("existing.kicad_sch")
|
|
203
|
+
await fs.writeFile("existing.kicad_sch", updated.getString())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Any class exposes `getChildren()` if you need to walk the tree manually, and snapshot tests (`bun test`) ensure the emitted S-expression stays identical to KiCad’s own formatting.
|
package/biome.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
|
3
|
+
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
|
4
|
+
"formatter": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"indentStyle": "space"
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"includes": ["**", "!**/cosmos-export", "!**/dist", "!**/package.json"]
|
|
10
|
+
},
|
|
11
|
+
"javascript": {
|
|
12
|
+
"formatter": {
|
|
13
|
+
"jsxQuoteStyle": "double",
|
|
14
|
+
"quoteProperties": "asNeeded",
|
|
15
|
+
"trailingCommas": "all",
|
|
16
|
+
"semicolons": "asNeeded",
|
|
17
|
+
"arrowParentheses": "always",
|
|
18
|
+
"bracketSpacing": true,
|
|
19
|
+
"bracketSameLine": false
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"linter": {
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"rules": {
|
|
25
|
+
"recommended": true,
|
|
26
|
+
"suspicious": {
|
|
27
|
+
"noExplicitAny": "off"
|
|
28
|
+
},
|
|
29
|
+
"complexity": {
|
|
30
|
+
"noForEach": "error",
|
|
31
|
+
"useLiteralKeys": "off"
|
|
32
|
+
},
|
|
33
|
+
"a11y": {
|
|
34
|
+
"noAccessKey": "off",
|
|
35
|
+
"noAriaHiddenOnFocusable": "off",
|
|
36
|
+
"noAriaUnsupportedElements": "off",
|
|
37
|
+
"noAutofocus": "off",
|
|
38
|
+
"noDistractingElements": "off",
|
|
39
|
+
"noHeaderScope": "off",
|
|
40
|
+
"noInteractiveElementToNoninteractiveRole": "off",
|
|
41
|
+
"noLabelWithoutControl": "off",
|
|
42
|
+
"noNoninteractiveElementToInteractiveRole": "off",
|
|
43
|
+
"noNoninteractiveTabindex": "off",
|
|
44
|
+
"noPositiveTabindex": "off",
|
|
45
|
+
"noRedundantAlt": "off",
|
|
46
|
+
"noRedundantRoles": "off",
|
|
47
|
+
"noStaticElementInteractions": "off",
|
|
48
|
+
"noSvgWithoutTitle": "off",
|
|
49
|
+
"useAltText": "off",
|
|
50
|
+
"useAnchorContent": "off",
|
|
51
|
+
"useAriaActivedescendantWithTabindex": "off",
|
|
52
|
+
"useAriaPropsForRole": "off",
|
|
53
|
+
"useAriaPropsSupportedByRole": "off",
|
|
54
|
+
"useButtonType": "off",
|
|
55
|
+
"useFocusableInteractive": "off",
|
|
56
|
+
"useHeadingContent": "off",
|
|
57
|
+
"useHtmlLang": "off",
|
|
58
|
+
"useIframeTitle": "off",
|
|
59
|
+
"useKeyWithClickEvents": "off",
|
|
60
|
+
"useKeyWithMouseEvents": "off",
|
|
61
|
+
"useMediaCaption": "off",
|
|
62
|
+
"useSemanticElements": "off",
|
|
63
|
+
"useValidAnchor": "off",
|
|
64
|
+
"useValidAriaProps": "off",
|
|
65
|
+
"useValidAriaRole": "off",
|
|
66
|
+
"useValidAriaValues": "off",
|
|
67
|
+
"useValidAutocomplete": "off",
|
|
68
|
+
"useValidLang": "off"
|
|
69
|
+
},
|
|
70
|
+
"style": {
|
|
71
|
+
"useSingleVarDeclarator": "error",
|
|
72
|
+
"noParameterAssign": "off",
|
|
73
|
+
"noUselessElse": "off",
|
|
74
|
+
"noNonNullAssertion": "off",
|
|
75
|
+
"useNumberNamespace": "off",
|
|
76
|
+
"noUnusedTemplateLiteral": "off",
|
|
77
|
+
"useFilenamingConvention": {
|
|
78
|
+
"level": "error",
|
|
79
|
+
"options": {
|
|
80
|
+
"strictCase": true,
|
|
81
|
+
"requireAscii": true,
|
|
82
|
+
"filenameCases": ["kebab-case", "export"]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"useAsConstAssertion": "error",
|
|
86
|
+
"useDefaultParameterLast": "error",
|
|
87
|
+
"useEnumInitializers": "error",
|
|
88
|
+
"useSelfClosingElements": "error",
|
|
89
|
+
"noInferrableTypes": "error"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/bun.lock
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "kicadts",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@biomejs/biome": "^2.2.4",
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"typescript": "^5",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
"packages": {
|
|
16
|
+
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
|
|
17
|
+
|
|
18
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
|
|
19
|
+
|
|
20
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
|
|
21
|
+
|
|
22
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
|
|
23
|
+
|
|
24
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
|
|
25
|
+
|
|
26
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
|
|
27
|
+
|
|
28
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
|
|
29
|
+
|
|
30
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
|
|
31
|
+
|
|
32
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
|
|
33
|
+
|
|
34
|
+
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
|
35
|
+
|
|
36
|
+
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
|
37
|
+
|
|
38
|
+
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
|
39
|
+
|
|
40
|
+
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
|
41
|
+
|
|
42
|
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
43
|
+
|
|
44
|
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
|
45
|
+
|
|
46
|
+
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
|
47
|
+
}
|
|
48
|
+
}
|
package/bunfig.toml
ADDED
package/lib/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./sexpr"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseToPrimitiveSExpr,
|
|
3
|
+
type PrimitiveSExpr,
|
|
4
|
+
} from "../parseToPrimitiveSExpr"
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PARENT_TOKEN = "__default__"
|
|
7
|
+
|
|
8
|
+
export abstract class SxClass {
|
|
9
|
+
abstract token: string
|
|
10
|
+
static token: string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Token strings are sometimes re-used (e.g. a "type" token) but the class
|
|
14
|
+
* varies based on the parent token
|
|
15
|
+
*/
|
|
16
|
+
static parentToken?: string
|
|
17
|
+
|
|
18
|
+
isSxClass = true
|
|
19
|
+
|
|
20
|
+
getChildren(): SxClass[] {
|
|
21
|
+
// By default, return any properties found in this instance that have the _sx* prefix
|
|
22
|
+
return Object.keys(this)
|
|
23
|
+
.filter((k) => k.startsWith("_sx"))
|
|
24
|
+
.map((k) => (this as any)[k])
|
|
25
|
+
.filter((v) => v && typeof v === "object" && v.isSxClass)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getStringIndented(): string {
|
|
29
|
+
return this.getString()
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => ` ${line}`)
|
|
32
|
+
.join("\n")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getString(): string {
|
|
36
|
+
const children = this.getChildren()
|
|
37
|
+
if (children.length === 0) {
|
|
38
|
+
return `(${this.token})`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = [`(${this.token}`]
|
|
42
|
+
for (const p of children) {
|
|
43
|
+
lines.push(p.getStringIndented())
|
|
44
|
+
}
|
|
45
|
+
lines.push(")")
|
|
46
|
+
return lines.join("\n")
|
|
47
|
+
}
|
|
48
|
+
get [Symbol.toStringTag](): string {
|
|
49
|
+
return this.getString()
|
|
50
|
+
}
|
|
51
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
52
|
+
return this.getString()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// =========================== STATIC METHODS ===========================
|
|
56
|
+
|
|
57
|
+
static classes: Record<string, Record<string, any>> = {}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Should be called after class definition to register the class for parsing
|
|
61
|
+
*/
|
|
62
|
+
static register(newClass: any) {
|
|
63
|
+
if (!newClass.token) {
|
|
64
|
+
throw new Error("Class must have a static override token")
|
|
65
|
+
}
|
|
66
|
+
const parentKey = newClass.parentToken ?? DEFAULT_PARENT_TOKEN
|
|
67
|
+
const existing = SxClass.classes[newClass.token] ?? {}
|
|
68
|
+
existing[parentKey] = newClass
|
|
69
|
+
SxClass.classes[newClass.token] = existing
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse an S-expression string into registered SxClass instances
|
|
74
|
+
*/
|
|
75
|
+
static parse(sexpr: string): SxClass[] {
|
|
76
|
+
const primitiveSexpr = parseToPrimitiveSExpr(sexpr)
|
|
77
|
+
|
|
78
|
+
return SxClass.parsePrimitiveSexpr(primitiveSexpr) as any
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static fromSexprPrimitives(primitiveSexprs: PrimitiveSExpr[]): SxClass {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`"${this.name}" class has not implemented fromSexprPrimitives`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static parsePrimitiveSexpr(
|
|
88
|
+
primitiveSexpr: PrimitiveSExpr,
|
|
89
|
+
options: { parentToken?: string } = {},
|
|
90
|
+
): SxClass | SxClass[] | number | string | boolean | null {
|
|
91
|
+
const parentToken = options.parentToken
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
Array.isArray(primitiveSexpr) &&
|
|
95
|
+
primitiveSexpr.length >= 1 &&
|
|
96
|
+
typeof primitiveSexpr[0] === "string"
|
|
97
|
+
) {
|
|
98
|
+
const classToken = primitiveSexpr[0] as string
|
|
99
|
+
const classGroup = SxClass.classes[classToken]
|
|
100
|
+
if (!classGroup) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Class "${classToken}" not registered via SxClass.register`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
const parentKey = parentToken ?? DEFAULT_PARENT_TOKEN
|
|
106
|
+
const ClassDef: any =
|
|
107
|
+
classGroup[parentKey] ?? classGroup[DEFAULT_PARENT_TOKEN]
|
|
108
|
+
if (!ClassDef) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Class "${classToken}" not registered for parent "${parentToken ?? "<root>"}"`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const args = primitiveSexpr.slice(1) as PrimitiveSExpr[]
|
|
114
|
+
if (!("fromSexprPrimitives" in ClassDef)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Class "${classToken}" does not have a fromSexprPrimitives method`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
const classInstance = ClassDef.fromSexprPrimitives(args)
|
|
120
|
+
return classInstance
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(primitiveSexpr)) {
|
|
124
|
+
return primitiveSexpr.map((item) =>
|
|
125
|
+
SxClass.parsePrimitiveSexpr(item, options),
|
|
126
|
+
) as any[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
typeof primitiveSexpr === "number" ||
|
|
131
|
+
typeof primitiveSexpr === "string" ||
|
|
132
|
+
typeof primitiveSexpr === "boolean" ||
|
|
133
|
+
primitiveSexpr === null
|
|
134
|
+
) {
|
|
135
|
+
return primitiveSexpr as number | string | boolean | null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Couldn't parse primitive S-expression: ${JSON.stringify(primitiveSexpr)}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =========================== STATIC UTILITIES ===========================
|
|
144
|
+
static parsePrimitivesToClassProperties(
|
|
145
|
+
primitiveSexprs: PrimitiveSExpr[],
|
|
146
|
+
parentToken?: string,
|
|
147
|
+
): {
|
|
148
|
+
propertyMap: Record<string, SxClass>
|
|
149
|
+
arrayPropertyMap: Record<string, SxClass[]>
|
|
150
|
+
} {
|
|
151
|
+
const propertyMap = {} as Record<string, SxClass>
|
|
152
|
+
const arrayPropertyMap = {} as Record<string, SxClass[]>
|
|
153
|
+
for (const primitiveSexpr of primitiveSexprs) {
|
|
154
|
+
const sxClass = SxClass.parsePrimitiveSexpr(primitiveSexpr, {
|
|
155
|
+
parentToken,
|
|
156
|
+
}) as SxClass
|
|
157
|
+
if (!sxClass.isSxClass) continue
|
|
158
|
+
propertyMap[sxClass.token] = sxClass
|
|
159
|
+
arrayPropertyMap[sxClass.token] ??= []
|
|
160
|
+
arrayPropertyMap[sxClass.token]!.push(sxClass)
|
|
161
|
+
}
|
|
162
|
+
return { propertyMap, arrayPropertyMap }
|
|
163
|
+
}
|
|
164
|
+
}
|