← TomSpecs
Publishedlicense: BSD-3-Clause

tom_doc_scanner

tom_doc_scanner · v0.2.0

The markdown structure parser the DocSpecs tooling reads with: headings, section ids and content become a typed document tree.

View repository → See License
Status
Published
LOC
828
Tests
83
Test LOC
1.3k

Overview

From the module readme.md file:

What it enables

Enables Structured markdown parsing, Section-id addressing.

Relationships

Standalone — no declared relationships.

tom_doc_scanner — markdown into section trees

> Cross-references. > tom_specs_model/doc/som_multiplatform_spec_model.md > owns the markdown format TomSpecs specifications are written in — the > headline-comment section ids, the uncapped heading depth and the parse > contract (SOM §11), and the schemas those documents are validated against > (SOM §13). The > DocSpecs specification > owns the DocSpecs format itself. This README is the catalogue of what > this parser produces and how to drive it; those documents own what a > conforming document looks like and why.

Parses markdown files into structured document trees with sections.

Where this fits

tom_doc_scanner turns a markdown file into a traversable tree of sections, each with a stable id, its source line, its raw headline, its key/value headline fields and its body text. It exists because no general-purpose CommonMark parser produces that shape: renderers give you HTML, AST libraries give you inline nodes, and both stop at ###### — whereas a machine-processed document needs identity (<!--[install]-->), provenance (which file, which line, which project) and unbounded nesting, because a document's section depth is a property of the document, not of the six levels HTML happens to have.

Inside TomSpecs — the method that builds software from structured specification documents — it is the bottom layer of the document tier. The layer directly above is tom_doc_specs, which adds schemas, typed sections and validation to the trees this package produces; above that, tom_specs_clitool generates the DocSpecs schemas that TomSpecs specifications are validated against, from the Specification Object Model in tom_specs_model. The eight non-Dart SOM runtimes embed their own parse/validate surface rather than a port of this one (SOM §14), so this package is the Dart plane's reader, not a shared kernel — which is also why it is useful entirely outside TomSpecs, and is used that way by tom_markdown_merge and the Forge markdown editor.

Overview

A scan is a pure function from text to a tree. DocScanner reads a file, MarkdownParser finds every headline in it, and the headlines are folded into a nested Section tree by their # level: a heading becomes the child of the nearest preceding heading of a lower level, and the text between one heading and the next becomes that section's text.

Three things are extracted from a headline beyond its title:

  • The id. Either declared — [install] or <!--[install]--> — or derived.

A single-word headline lowercases to its own id (Usage → usage); a multi-word one takes parent.index (intro.0), which is stable under editing of the text but not under reordering, so any id a consumer intends to keep should be declared. - Key/value fields. status="shipped", owner="ada" inside the headline comment is parsed into a Map<String, String> and removed from the title. - The raw headline, kept verbatim, so a tool that rewrites a document can reproduce the line it came from rather than reconstructing it.

A Document is a Section with file provenance attached — the absolute path, the workspace-relative path, the owning project and its root, and the maximum heading depth found. A DocumentFolder is a directory of them, nested to mirror the directory tree, with allDocuments flattening it.

Every entry point comes in an async and a Sync form, and every one accepts a DocScannerFactory so a caller can substitute its own Section / Document subclasses — which is exactly how tom_doc_specs layers SpecSection and SpecDoc on top without this package knowing about schemas.

Installation

dependencies:
  tom_doc_scanner: ^0.1.0
dart pub add tom_doc_scanner

For the command-line tool:

dart pub global activate tom_doc_scanner

Features

Parsing

FeatureBehaviour
Headline levels # through any number of # — no upper bound . Levels beyond 6 are parsed as sections; CommonMark renderers show ####### as literal text, but for a machine format the tree is authoritative (SOM §11.2)
Explicit ids ## [install] Installation or ## <!--[install]--> Installation — the comment form keeps the id out of the rendered preview
Derived idsSingle word → lowercased; otherwise parent.index
Headline fields key="value" pairs parsed out of the headline into Section.fields
Raw headlineThe source line kept verbatim in Section.rawHeadline
Section text Everything between one headline and the next, in Section.text

Provenance

FeatureBehaviour
Line numbers1-based source line of every headline
Workspace paths Path relative to a workspaceRoot you supply (default: the current directory)
Project detection Owning project — the first path segment below the workspaceRoot — with the project-relative path and the project root
Hierarchy depthThe deepest heading level in the document

Entry points

FeatureBehaviour
One fileDocScanner.scanDocument / scanDocumentSync
Several files DocScanner.scanDocuments / scanDocumentsSync
A directory tree DocScanner.scanTree / scanTreeSync, recursing into subfolders
Custom node types A DocScannerFactory substitutes your own Section / Document subclasses
JSON toJson / fromJson on every model, and a doc_scanner CLI that writes the files

Quick start

import 'dart:io';
import 'package:tom_doc_scanner/tom_doc_scanner.dart';

Future<void> main() async {
  File('guide.md').writeAsStringSync('''
# Guide

How to use the thing.

## <!--[install]--> Installation

Run the installer.

### Troubleshooting

Check the log.

## Usage

Call it.
''');

  final doc = await DocScanner.scanDocument(filepath: 'guide.md');

  print('${doc.name}  (depth ${doc.hierarchyDepth})');
  for (final s in doc.sections!) {
    print('  ${s.id.padRight(10)} ${s.name}  [${s.sections?.length ?? 0} sub]');
  }
}

// Guide  (depth 3)
//   install    Installation  [1 sub]
//   usage      Usage  [0 sub]

Two things to read off that output. install is the id the document declared in its headline comment; usage was derived, because "Usage" is a single word. And Troubleshooting does not appear at the top level — it is the one sub-section of Installation, because ### nests under the ## above it.

Usage

Scanning a directory tree

final folder = DocScanner.scanTreeSync(path: 'demo');
print('documents: ${folder.allDocuments.length}');
// documents: 2

scanTree mirrors the directory structure: folder.folders holds the subdirectories as further DocumentFolders and folder.documents the markdown files at this level. allDocuments flattens the whole tree when the structure does not matter.

Reading a section

// demo/a.md:
//   # Release Notes
//
//   ## <!--[r12] status="shipped", owner="ada"--> Release 1.2
//
//   Shipped on Tuesday.

final s = DocScanner.scanDocumentSync(filepath: 'demo/a.md').sections!.first;

print('id=${s.id} name=${s.name}');   // id=r12 name=Release 1.2
print('fields=${s.fields}');          // fields={status: shipped, owner: ada}
print('line=${s.lineNumber}');        // line=3
print('text=${s.text.trim()}');       // text=Shipped on Tuesday.

The key/value pairs are parsed out of the headline and are not part of name — the title is left clean. rawHeadline still holds the original line in full, for a tool that has to write the document back.

Custom node types

class TaggedSection extends Section {
  TaggedSection({
    required super.index, required super.lineNumber,
    required super.rawHeadline, required super.name,
    required super.id, required super.text,
    required super.fields, super.sections,
  });

  bool get isDraft => fields['status'] == 'draft';
}

class TaggedFactory extends DocScannerFactory {
  const TaggedFactory();

  @override
  Section createSection({
    required int index, required int lineNumber,
    required String rawHeadline, required String name,
    required String id, required String text,
    required Map<String, String> fields, List<Section>? sections,
  }) => TaggedSection(
        index: index, lineNumber: lineNumber,
        rawHeadline: rawHeadline, name: name,
        id: id, text: text,
        fields: fields, sections: sections,
      );
}

final doc = DocScanner.scanDocumentSync(
  filepath: 'demo/a.md',
  factory: const TaggedFactory(),
);
final s = doc.sections!.first as TaggedSection;

print('${s.id}  draft=${s.isDraft}');   // r12  draft=false

Override createSection to change what the tree is made of, createDocument to change the root node; the scanner calls them for every node it builds, so a whole scan comes back in your own types.

This is the extension point tom_doc_specs uses: it passes a factory that builds SpecSection / SpecDoc, so the schema layer gets typed nodes out of an unmodified scan.

Command line

# One file → guide.json in the current directory
dart run tom_doc_scanner:doc_scanner scandocument guide.md

# Several files into an output folder
dart run tom_doc_scanner:doc_scanner scandocuments a.md b.md -target=output

# A whole tree, mirroring the directory structure
dart run tom_doc_scanner:doc_scanner scantree docs/ -target=json-output

# The same tree, all output files in one flat folder
dart run tom_doc_scanner:doc_scanner scantree docs/ -flat -target=all-json

-overwrite replaces existing output instead of renaming around it; -help prints the full option list. Each markdown file becomes a JSON file with the same base name, holding the section tree and the document's path information.

Architecture

  markdown text
        │
        ▼
  MarkdownParser ......... finds headlines, extracts ids and key/value fields
        │  (ParsedHeadline, startLine, endLine)
        ▼
  DocScanner ............. folds headlines into a tree by level,
        │                  attaches text, resolves file provenance
        │  creates nodes through
        ▼
  DocScannerFactory ...... substitution point for custom node types
        │
        ▼
  Document  ─┬─ Section (nested)
             └─ path / project / depth information

  DocumentFolder ......... a directory of Documents, nested
TypeResponsibility
DocScanner The entry points — scanDocument , scanDocuments , scanTree , each with a Sync twin. Holds the tree-building and provenance logic
MarkdownParser Headline recognition, id extraction (both syntaxes), key/value field parsing, id generation and depth calculation. Pure and static
ParsedHeadline One recognised headline before it becomes a node: level, name, id, raw text, fields
Section A headline and its content — index , lineNumber , rawHeadline , name , id , text , fields , and nested sections
Document A Section with file provenance: paths, owning project, load timestamp, hierarchyDepth
DocumentFolder A directory of documents and subfolders, with allDocuments flattening the tree
DocScannerFactory Creates Section and Document instances; subclass it to have the scanner build your own types

Ecosystem

              tom_doc_scanner        ← this package (depends only on `path`)
                     ▲
      ┌──────────────┴───────────────┬─────────────────────┐
      │  section trees               │                     │
 tom_doc_specs               tom_markdown_merge      tom_md_editor
 schemas, typed sections,    document merging        the Forge markdown
 validation                                          editor
      ▲
      │  DocSpecs documents + schemas
 tom_specs_clitool ......... generates *.docspecs-schema.yaml from the SOM
      ▲
      │
 tom_specs_model ........... the Specification Object Model

The upward chain is TomSpecs; the two branches on the right are not. Nothing in this package knows about schemas, TomSpecs or the SOM — that is what lets those three consumers sit side by side.

Further documentation

TomSpecs subject matter — the authorities this package's readers need:

DocumentAuthority for
index.md The catalogue of the whole TomSpecs document set, and the § citation convention used throughout it
som_multiplatform_spec_model.md The markdown format TomSpecs documents use — section ids, uncapped depth, the parse contract (SOM §11) — the schemas they are validated against (SOM §13), and why the non-Dart runtimes embed their own reader instead of porting this one (SOM §14)
doc_specs_specification.md The DocSpecs format itself: schemas, section types, validation rules
tom_specs_project_flow.md The TomSpecs creation process — the phases whose documents this parser reads

Siblings — packages you will reach for next:

PackageWhat it is
tom_doc_specs Schemas, typed sections and validation on top of these trees
tom_specs_clitool Generates the DocSpecs schemas TomSpecs documents are validated against
tom_specs_model The Specification Object Model those schemas are generated from

Status

Version 0.1.0, published on pub.dev. 74 tests, all passing.

License
BSD 3-Clause License

Copyright (c) 2024-2026, Peter Nicolai Alexis Kyaw
Find me on LinkedIn under Alexis Kyaw
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.