Skip to content

Latest commit

 

History

History
118 lines (85 loc) · 3.93 KB

File metadata and controls

118 lines (85 loc) · 3.93 KB

Contributing to SubstackDown

Setup

git clone https://github.com/substackdown/substackdown.git
cd substackdown
npm install
npm run build
npm test

For the VS Code extension:

cd vscode-extension
npm install
npm run build

Project Structure

src/
  parser.ts          .sub text -> AST (hand-rolled line-by-line parser)
  emitter.ts         AST -> ProseMirror JSON (Substack's draft_body format)
  decompiler.ts      ProseMirror JSON -> AST -> .sub text
  html-emitter.ts    AST -> Substack-styled HTML (for preview)
  cli.ts             CLI entry point, all commands
  sync.ts            Substack API client (auth, push, pull, publish)
  manifest.ts        .substackdown/manifest.json CRUD and state tracking
  validate.ts        Frontmatter validation
  types.ts           AST type definitions (BlockNode, InlineNode, Mark)
  index.ts           Public API exports

  parser.test.ts     108 tests — parsing, round-trips, integration
  manifest.test.ts   33 tests — CRUD, hashing, state machine
  sync.test.ts       39 tests — auth, API mocking, retries, errors
  cli.test.ts        43 tests — all CLI commands via subprocess
  validate.test.ts   17 tests — frontmatter validation
  export.test.ts     27 tests — markdown export conversion

vscode-extension/
  src/extension.ts   VS Code extension entry point
  src/webview.ts     TipTap-based preview panel
  syntaxes/          TextMate grammar for .sub files

Running Tests

npm run build     # TypeScript -> JavaScript
npm test          # Run all tests via Node's built-in test runner
npm run test:src  # Run tests directly from TypeScript (uses tsx)

Tests use Node's built-in test runner (node:test + node:assert/strict). No external test framework needed.

How the Pipeline Works

.sub text -> parse() -> SubDocument { frontmatter, body: BlockNode }
                                          |
                              emit(body) -> ProseMirror JSON string
                                          |
                          decompile(json) -> SubDocument
                                          |
                           render(doc) -> .sub text

The round-trip must be idempotent. If you change the parser, verify with:

substackdown roundtrip test/bugwoman.sub

Adding a New @-Block Type

1. Add the AST type

In src/types.ts, add a new variant to the BlockNode union:

| { type: 'my_widget'; someAttr: string; sourceLine?: number }

2. Add parser support

In src/parser.ts, update the @-block parsing section. For self-closing blocks, add to the attribute extraction switch. For content blocks, add to the content block handler.

3. Add emitter support

In src/emitter.ts, add a case to emitBlock() that produces the ProseMirror JSON node Substack expects. Check Substack's editor source for the exact PM node type and attributes.

4. Add decompiler support

In src/decompiler.ts, add a case to decompileBlock() that converts the PM JSON node back to your AST type. Also add to renderBlock() to produce the .sub text.

5. Add tests

Add parsing tests in src/parser.test.ts and a round-trip test. The round-trip test is the most important — it ensures parse -> emit -> decompile -> render reproduces the input.

6. Update the HTML emitter

In src/html-emitter.ts, add rendering for the preview.

Code Conventions

  • TypeScript with strict mode
  • ESM modules (.js extensions in imports)
  • No external runtime dependencies (the CLI is zero-dependency)
  • Tests use describe/it from node:test
  • Temp directories for filesystem tests, mock.fn() for fetch mocking
  • Commit messages: imperative mood, explain the "why"

Frozen Files

The core pipeline files (parser.ts, emitter.ts, decompiler.ts, types.ts) should only be modified when adding new block types or fixing parser bugs. Changes to these files require round-trip verification against test/draft-body.json.