Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a35b60d
Update the file test to pass in a file name
mwittie Apr 24, 2026
bfe8093
Generate output in BashCell.Execute
mwittie Apr 24, 2026
72c2ef0
Separate scanning of BashCell output into a separate file
mwittie Apr 24, 2026
1339135
Create an Output struct
mwittie Apr 24, 2026
1ba8390
Simplify Classify with BashCellFromBlocks
mwittie Apr 24, 2026
6538cc2
Handle missing closing output market
mwittie Apr 25, 2026
160bd10
Handle fenced block indents
mwittie Apr 25, 2026
c6b232c
Add tests for MakeOutput and WithIndent
mwittie Apr 27, 2026
efe6d22
Add missing Output.Render tests
mwittie Apr 27, 2026
015a76c
Make list indented blocks come with indent as prefix
mwittie Apr 27, 2026
de67301
Handle output indent classifying blocks
mwittie Apr 27, 2026
ada3f74
Merge branch 'main-litdoc' into feature/generate-and-copy-output-blocks
mwittie Apr 30, 2026
9f5f564
Remove make test dependency on build
mwittie May 1, 2026
6165275
Unify go caching across local, claude sandbox, and CI
mwittie May 1, 2026
af89cf0
Adapt to the new behavior of MakeBlocksFromMarkdown
mwittie May 1, 2026
259c91b
Don't verify if output indent same as that of the bash cell
mwittie May 1, 2026
d2f23cb
Handle indented output in input markdown
mwittie May 1, 2026
46fe595
Extend TestProcessFile to test idempotency
mwittie May 4, 2026
d77db1d
Simplify Output implementation
mwittie May 4, 2026
31d1c6b
Validate litdoc output indentation
mwittie May 4, 2026
133155c
Fix validateInlineMarkerPrefix error to report b.indent as "got"
mwittie May 4, 2026
74ecad5
Check f.Close() error in TestProcessFileNestedListIndent
mwittie May 4, 2026
2928f80
Comment the parser quirks behind skipWhitespaceLines and normalizeHTM…
mwittie May 4, 2026
1600298
Exclude vendor from fmt and fmt-check targets
mwittie May 4, 2026
97a29a2
Add mockery to flake.nix dev shell packages
mwittie May 4, 2026
b1128b9
Fix mockery nix package name to go-mockery
mwittie May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ jobs:
name: litdoc
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

- uses: actions/cache@v4
with:
path: .go-cache
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-build-

- run: nix develop --ignore-environment --command make vendor

- run: nix develop --ignore-environment --command make fmt-check
- run: nix develop --ignore-environment --command make vet
- run: nix develop --ignore-environment --command make test
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# vendor/
# Dependency directories
vendor/

# Go workspace file
go.work
Expand All @@ -28,6 +28,9 @@ go.work.sum
# env file
.env

# Go build cache
.go-cache/

# Editor/IDE
.idea/
# .vscode/
Expand Down
25 changes: 17 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
GO ?= go

.PHONY: pre-pr
pre-pr: clean mock fmt-check vet test

.PHONY: fmt
fmt:
@gofmt -w .
@gofmt -w $(GO_FILES)

.PHONY: fmt-check
fmt-check:
@unformatted=$$(gofmt -l .); \
@unformatted=$$(gofmt -l $(GO_FILES)); \
if [ -n "$$unformatted" ]; then \
echo "unformatted files:"; \
echo "$$unformatted"; \
Expand All @@ -16,7 +18,7 @@ fmt-check:

.PHONY: vet
vet:
@go vet ./...
@$(GO) vet ./...

.PHONY: mock
mock:
Expand All @@ -27,17 +29,24 @@ mock-clean:
@find . \( -name '*_mock_test.go' -o -name '*_mock.go' \) -not -path './vendor/*' -delete

GO_FILES := $(shell find . -name '*.go' -not -path './vendor/*')
GOCACHE ?= /tmp/litdoc-go-build
GOCACHE ?= $(CURDIR)/.go-cache

vendor: go.mod go.sum
@$(GO) mod vendor

bin/litdoc: $(GO_FILES)
@GOCACHE=$(GOCACHE) go build -o bin/litdoc .
bin/litdoc: vendor $(GO_FILES)
@GOCACHE=$(GOCACHE) $(GO) build -mod=vendor -o bin/litdoc .

.PHONY: build
build: bin/litdoc

.PHONY: test
test: build
@GOCACHE=$(GOCACHE) go test ./... --count=1
test: vendor
@GOCACHE=$(GOCACHE) $(GO) test -mod=vendor ./... --count=1

.PHONY: vendor-clean
vendor-clean:
@rm -rf vendor/

.PHONY: clean
clean: mock-clean
Expand Down
3 changes: 2 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go
go-mockery
cacert
];
shellHook = ''
export HOME=$(mktemp -d)
export GOPATH=$HOME/go
export GOCACHE=$HOME/.cache/go-build
export GOCACHE=$PWD/.go-cache
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
'';
Expand Down
66 changes: 62 additions & 4 deletions internal/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ func (c *blockCollector) collectBlockQuote(
indent []byte,
stripPrefix []byte,
) {
c.appendTextGap(node.StartByte(), stripPrefix, indent)
if c.pos < node.StartByte() {
c.pos = node.StartByte()
}

childIndent := concatenate(indent, []byte("> "))
childStripPrefix := concatenate(stripPrefix, []byte("> "))
for i := 0; i < int(node.ChildCount()); i++ {
Expand Down Expand Up @@ -143,7 +148,11 @@ func (c *blockCollector) collectLeaf(node *sitter.Node, indent, stripPrefix []by
end := node.EndByte()
blockIndent, blockStripPrefix := blockPrefixes(node, c.content, indent, stripPrefix)

c.appendTextGap(start, stripPrefix, indent)
gapStripPrefix := stripPrefix
if isOutputMarkerRaw(c.content[start:end]) {
gapStripPrefix = blockStripPrefix
}
c.appendTextGap(start, gapStripPrefix, blockIndent)

raw := stripIndent(c.content[start:end], blockStripPrefix)
kind := blockKind(node, c.content)
Expand Down Expand Up @@ -171,6 +180,9 @@ func (c *blockCollector) appendTextGap(end uint32, stripPrefix, indent []byte) {
return
}
gap := stripIndent(c.content[c.pos:end], stripPrefix)
if len(bytes.TrimSpace(gap)) == 0 {
indent = []byte(renderIndent(string(indent)))
}
c.appendBlock(BlockKindText, gap, indent)
}

Expand Down Expand Up @@ -242,6 +254,14 @@ func blockPrefixes(
if len(linePrefix) > 0 && !bytes.Equal(linePrefix, indent) {
blockIndent = linePrefix
blockStripPrefix = linePrefix
if normalizedIndent, ok := normalizeHTMLCommentPrefix(
content[start:end],
linePrefix,
indent,
); ok {
blockIndent = normalizedIndent
return blockIndent, blockStripPrefix
}
if isSpaceIndent(linePrefix) {
rawLeading := leadingLineSpaces(content[start:end])
if len(rawLeading) > 0 {
Expand All @@ -266,6 +286,30 @@ func blockPrefixes(
return blockIndent, blockStripPrefix
}

// normalizeHTMLCommentPrefix handles a tree-sitter quirk: output markers
// inside list items are reported with a space-only linePrefix (the list
// continuation indent) rather than the rendered block indent. Detect this case
// and return the rendered indent so the marker is classified correctly.
func normalizeHTMLCommentPrefix(raw, linePrefix, containerIndent []byte) ([]byte, bool) {
renderedIndent := []byte(renderIndent(string(containerIndent)))
if !bytes.HasPrefix(linePrefix, renderedIndent) {
return nil, false
}
if len(bytes.Trim(linePrefix[len(renderedIndent):], " ")) > 0 {
return nil, false
}
if !isOutputMarkerRaw(raw) {
return nil, false
}
return renderedIndent, true
}

func isOutputMarkerRaw(raw []byte) bool {
raw = bytes.TrimLeft(raw, " ")
return bytes.HasPrefix(raw, []byte(OutputBeginMarker)) ||
bytes.HasPrefix(raw, []byte(OutputEndMarker))
}

func isClosedFencedCodeBlock(content []byte) bool {
openLineEnd := bytes.IndexByte(content, '\n')
if openLineEnd < 0 {
Expand Down Expand Up @@ -474,7 +518,9 @@ func splitInlineHTMLComments(blocks []Block) []Block {
}
pos := 0
for _, loc := range locs {
if loc[0] > pos {
wholeBlock := isWholeTextBlock(content, loc)
outputMarker := isOutputMarkerContent(content[loc[0]:loc[1]])
if loc[0] > pos && !isWholeBlockPrefix(content[pos:loc[0]], wholeBlock, outputMarker) {
result = append(
result,
MakeBlockFromRaw(
Expand All @@ -485,15 +531,18 @@ func splitInlineHTMLComments(blocks []Block) []Block {
),
)
}
wholeBlock := isWholeTextBlock(content, loc)
commentContinuation := b.continuation
if !wholeBlock {
commentContinuation = b.continuation || loc[0] > 0
}
commentIndent := b.indent
if wholeBlock && outputMarker && b.indent == "" {
commentIndent = content[pos:loc[0]]
}
result = append(result, MakeBlockFromRaw(
BlockKindHTMLComment,
content[loc[0]:loc[1]],
b.indent,
commentIndent,
commentContinuation,
))
pos = loc[1]
Expand All @@ -508,6 +557,15 @@ func splitInlineHTMLComments(blocks []Block) []Block {
return result
}

func isWholeBlockPrefix(content string, wholeBlock, outputMarker bool) bool {
return wholeBlock && outputMarker && strings.TrimSpace(content) == ""
}

func isOutputMarkerContent(content string) bool {
return strings.HasPrefix(content, OutputBeginMarker) ||
strings.HasPrefix(content, OutputEndMarker)
}

func isWholeTextBlock(content string, loc []int) bool {
return len(strings.TrimSpace(content[:loc[0]])) == 0 &&
len(strings.TrimSpace(content[loc[1]:])) == 0
Expand Down
23 changes: 23 additions & 0 deletions internal/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,15 @@ func TestMakeBlocksFromMarkdown(t *testing.T) {
cmnt("", "<!-- comment -->", true),
},
},
{
name: "HTMLComment/top-level/leading-spaces",
input: " <!-- comment -->\n",
want: []internal.Block{
text("", " ", false),
cmnt("", "<!-- comment -->", false),
text("", "\n", true),
},
},
{
name: "HTMLComment/top-level/block",
input: joinLines(
Expand Down Expand Up @@ -524,6 +533,20 @@ func TestMakeBlocksFromMarkdown(t *testing.T) {
text("- ", "text", false),
},
},
{
name: "HTMLComment/list/dash/blank-before-indented-block",
input: joinLines(
"- item",
"",
" <!-- BEGIN LITDOC OUTPUT -->",
) + "\n",
want: []internal.Block{
text("- ", "item\n", false),
text(" ", "\n", false),
cmnt(" ", "<!-- BEGIN LITDOC OUTPUT -->", false),
text(" ", "\n", true),
},
},
{
name: "HTMLComment/list/dash/nested/block",
input: joinLines(
Expand Down
Loading
Loading