Skip to content

Commit 3faab11

Browse files
authored
Feature/editorial mark attributes (#64)
* document editorial mark attributes, fix qmd roundtripping
1 parent 95ea2a8 commit 3faab11

15 files changed

Lines changed: 109505 additions & 102251 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
target
1+
target
2+
.beads

CLAUDE.md

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,96 @@
33
The main documentation for this repository is located at:
44
[crates/quarto-markdown-pandoc/CLAUDE.md](crates/quarto-markdown-pandoc/CLAUDE.md)
55

6+
## **WORK TRACKING**
7+
8+
We use bd (beads) for issue tracking instead of Markdown TODOs or external tools.
9+
10+
### Quick Reference
11+
12+
```bash
13+
# Find ready work (no blockers)
14+
bd ready --json
15+
16+
# Create new issue
17+
bd create "Issue title" -t bug|feature|task -p 0-4 -d "Description" --json
18+
19+
# Create with explicit ID (for parallel workers)
20+
bd create "Issue title" --id worker1-100 -p 1 --json
21+
22+
# Create with labels
23+
bd create "Issue title" -t bug -p 1 -l bug,critical --json
24+
25+
# Create multiple issues from markdown file
26+
bd create -f feature-plan.md --json
27+
28+
# Update issue status
29+
bd update <id> --status in_progress --json
30+
31+
# Link discovered work (old way)
32+
bd dep add <discovered-id> <parent-id> --type discovered-from
33+
34+
# Create and link in one command (new way)
35+
bd create "Issue title" -t bug -p 1 --deps discovered-from:<parent-id> --json
36+
37+
# Label management
38+
bd label add <id> <label> --json
39+
bd label remove <id> <label> --json
40+
bd label list <id> --json
41+
bd label list-all --json
42+
43+
# Filter issues by label
44+
bd list --label bug,critical --json
45+
46+
# Complete work
47+
bd close <id> --reason "Done" --json
48+
49+
# Show dependency tree
50+
bd dep tree <id>
51+
52+
# Get issue details
53+
bd show <id> --json
54+
55+
# Import with collision detection
56+
bd import -i .beads/issues.jsonl --dry-run # Preview only
57+
bd import -i .beads/issues.jsonl --resolve-collisions # Auto-resolve
58+
```
59+
60+
### Workflow
61+
62+
1. **Check for ready work**: Run `bd ready` to see what's unblocked
63+
2. **Claim your task**: `bd update <id> --status in_progress`
64+
3. **Work on it**: Implement, test, document
65+
4. **Discover new work**: If you find bugs or TODOs, create issues:
66+
- Old way (two commands): `bd create "Found bug in auth" -t bug -p 1 --json` then `bd dep add <new-id> <current-id> --type discovered-from`
67+
- New way (one command): `bd create "Found bug in auth" -t bug -p 1 --deps discovered-from:<current-id> --json`
68+
5. **Complete**: `bd close <id> --reason "Implemented"`
69+
6. **Export**: Changes auto-sync to `.beads/issues.jsonl` (5-second debounce)
70+
71+
### Issue Types
72+
73+
- `bug` - Something broken that needs fixing
74+
- `feature` - New functionality
75+
- `task` - Work item (tests, docs, refactoring)
76+
- `epic` - Large feature composed of multiple issues
77+
- `chore` - Maintenance work (dependencies, tooling)
78+
79+
### Priorities
80+
81+
- `0` - Critical (security, data loss, broken builds)
82+
- `1` - High (major features, important bugs)
83+
- `2` - Medium (nice-to-have features, minor bugs)
84+
- `3` - Low (polish, optimization)
85+
- `4` - Backlog (future ideas)
86+
87+
### Dependency Types
88+
89+
- `blocks` - Hard dependency (issue X blocks issue Y)
90+
- `related` - Soft relationship (issues are connected)
91+
- `parent-child` - Epic/subtask relationship
92+
- `discovered-from` - Track issues discovered during work
93+
94+
Only `blocks` dependencies affect the ready work queue.
95+
696
## **CRITICAL - TEST-DRIVEN DEVELOPMENT**
797

898
When fixing ANY bug:
@@ -11,11 +101,11 @@ When fixing ANY bug:
11101
3. **THIRD**: Implement the fix
12102
4. **FOURTH**: Run the test and verify it passes
13103

14-
**This is non-negotiable. Never implement a fix before verifying the test fails.**
104+
**This is non-negotiable. Never implement a fix before verifying the test fails. Stop and ask the user if you cannot think of a way to mechanically test the bad behavior.**
15105

16106
## General Instructions
17107

18-
- in this repository, "qmd" means "quarto markdown", the dialect of markdown we are developing. Although we aim to be largely compatible with Pandoc, it is not necessarily the case that a discrepancy in the behavior is a bug.
108+
- in this repository, "qmd" means "quarto markdown", the dialect of markdown we are developing. Although we aim to be largely compatible with Pandoc, discrepancies in the behavior might not be bugs.
19109
- the qmd format only supports the inline syntax for a link [link](./target.html), and not the reference-style syntax [link][1].
20110
- Always strive for test documents as small as possible. Prefer a large number of small test documents instead of small number of large documents.
21111
- When fixing bugs, always try to isolate and fix one bug at a time.

crates/quarto-markdown-pandoc/resources/error-corpus/_autogen-table.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
{
33
"column": 17,
44
"row": 0,
5-
"state": 1133,
5+
"state": 1284,
66
"sym": "end",
77
"errorInfo": {
88
"title": "Unclosed Span",
99
"message": "I reached the end of the block before finding a closing ']' for the span or link.",
1010
"captures": [
1111
{
1212
"column": 3,
13-
"lrState": 241,
13+
"lrState": 301,
1414
"row": 0,
1515
"size": 1,
1616
"sym": "[",
@@ -30,23 +30,23 @@
3030
{
3131
"column": 20,
3232
"row": 0,
33-
"state": 2557,
33+
"state": 2633,
3434
"sym": "class_specifier",
3535
"errorInfo": {
3636
"title": "Key-value Pair Before Class Specifier in Attribute",
3737
"message": "This class specifier appears after the key-value pair.",
3838
"captures": [
3939
{
4040
"column": 10,
41-
"lrState": 2618,
41+
"lrState": 2743,
4242
"row": 0,
4343
"size": 3,
4444
"sym": "key_value_key",
4545
"label": "key-value-begin"
4646
},
4747
{
4848
"column": 14,
49-
"lrState": 2464,
49+
"lrState": 2548,
5050
"row": 0,
5151
"size": 5,
5252
"sym": "key_value_value_token1",
@@ -67,15 +67,15 @@
6767
{
6868
"column": 18,
6969
"row": 0,
70-
"state": 1926,
70+
"state": 2050,
7171
"sym": "_error",
7272
"errorInfo": {
7373
"title": "Mismatched Delimiter in Attribute Specifier",
7474
"message": "I expected a '}', language specifier, an identifier, a class specifier, or a key-value pair.",
7575
"captures": [
7676
{
7777
"column": 17,
78-
"lrState": 1926,
78+
"lrState": 2050,
7979
"row": 0,
8080
"size": 1,
8181
"sym": "{",

crates/quarto-markdown-pandoc/src/pandoc/inline.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,24 +218,28 @@ pub enum CitationMode {
218218

219219
#[derive(Debug, Clone, PartialEq)]
220220
pub struct Insert {
221+
pub attr: Attr,
221222
pub content: Inlines,
222223
pub source_info: SourceInfo,
223224
}
224225

225226
#[derive(Debug, Clone, PartialEq)]
226227
pub struct Delete {
228+
pub attr: Attr,
227229
pub content: Inlines,
228230
pub source_info: SourceInfo,
229231
}
230232

231233
#[derive(Debug, Clone, PartialEq)]
232234
pub struct Highlight {
235+
pub attr: Attr,
233236
pub content: Inlines,
234237
pub source_info: SourceInfo,
235238
}
236239

237240
#[derive(Debug, Clone, PartialEq)]
238241
pub struct EditComment {
242+
pub attr: Attr,
239243
pub content: Inlines,
240244
pub source_info: SourceInfo,
241245
}

crates/quarto-markdown-pandoc/src/pandoc/treesitter.rs

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use crate::pandoc::treesitter_utils::code_fence_content::process_code_fence_cont
1414
use crate::pandoc::treesitter_utils::code_span::process_code_span;
1515
use crate::pandoc::treesitter_utils::commonmark_attribute::process_commonmark_attribute;
1616
use crate::pandoc::treesitter_utils::document::process_document;
17+
use crate::pandoc::treesitter_utils::editorial_marks::{
18+
process_delete, process_editcomment, process_highlight, process_insert,
19+
};
1720
use crate::pandoc::treesitter_utils::fenced_code_block::process_fenced_code_block;
1821
use crate::pandoc::treesitter_utils::fenced_div_block::process_fenced_div_block;
1922
use crate::pandoc::treesitter_utils::indented_code_block::process_indented_code_block;
@@ -50,8 +53,7 @@ use crate::pandoc::treesitter_utils::uri_autolink::process_uri_autolink;
5053
use crate::pandoc::ast_context::ASTContext;
5154
use crate::pandoc::block::{Block, Blocks, BulletList, OrderedList, Paragraph, Plain, RawBlock};
5255
use crate::pandoc::inline::{
53-
Delete, EditComment, Emph, Highlight, Inline, Insert, Note, RawInline, Space, Str, Strikeout,
54-
Strong, Subscript, Superscript,
56+
Emph, Inline, Note, RawInline, Space, Str, Strikeout, Strong, Subscript, Superscript,
5557
};
5658
use crate::pandoc::list::{ListAttributes, ListNumberDelim, ListNumberStyle};
5759
use crate::pandoc::location::{
@@ -675,38 +677,10 @@ fn native_visitor<T: Write>(
675677
Strikeout,
676678
context
677679
),
678-
"insert" => emphasis_inline!(
679-
node,
680-
children,
681-
"insert_delimiter",
682-
native_inline,
683-
Insert,
684-
context
685-
),
686-
"delete" => emphasis_inline!(
687-
node,
688-
children,
689-
"delete_delimiter",
690-
native_inline,
691-
Delete,
692-
context
693-
),
694-
"highlight" => emphasis_inline!(
695-
node,
696-
children,
697-
"highlight_delimiter",
698-
native_inline,
699-
Highlight,
700-
context
701-
),
702-
"edit_comment" => emphasis_inline!(
703-
node,
704-
children,
705-
"edit_comment_delimiter",
706-
native_inline,
707-
EditComment,
708-
context
709-
),
680+
"insert" => process_insert(buf, node, children, context),
681+
"delete" => process_delete(buf, node, children, context),
682+
"highlight" => process_highlight(buf, node, children, context),
683+
"edit_comment" => process_editcomment(buf, node, children, context),
710684

711685
"quoted_span" => process_quoted_span(node, children, native_inline, context),
712686
"code_span" => process_code_span(buf, node, children, context),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* editorial_marks.rs
3+
*
4+
* Functions for processing editorial mark nodes in the tree-sitter AST.
5+
*
6+
* Copyright (c) 2025 Posit, PBC
7+
*/
8+
9+
use crate::pandoc::ast_context::ASTContext;
10+
use crate::pandoc::inline::{Delete, EditComment, Highlight, Inline, Inlines, Insert, Space, Str};
11+
use crate::pandoc::location::{SourceInfo, node_source_info_with_context};
12+
use once_cell::sync::Lazy;
13+
use regex::Regex;
14+
use std::collections::HashMap;
15+
use std::io::Write;
16+
17+
use super::pandocnativeintermediate::PandocNativeIntermediate;
18+
use super::text_helpers::apply_smart_quotes;
19+
20+
macro_rules! process_editorial_mark {
21+
($struct_name:ident) => {
22+
paste::paste! {
23+
pub fn [<process_ $struct_name:lower>]<T: Write>(
24+
buf: &mut T,
25+
node: &tree_sitter::Node,
26+
children: Vec<(String, PandocNativeIntermediate)>,
27+
context: &ASTContext,
28+
) -> PandocNativeIntermediate {
29+
let whitespace_re: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s+").unwrap());
30+
let mut attr = ("".to_string(), vec![], HashMap::new());
31+
let mut content: Inlines = vec![];
32+
33+
for (_node_name, child) in children {
34+
match child {
35+
PandocNativeIntermediate::IntermediateAttr(a) => {
36+
attr = a;
37+
}
38+
PandocNativeIntermediate::IntermediateInline(inline) => {
39+
content.push(inline);
40+
}
41+
PandocNativeIntermediate::IntermediateInlines(mut inlines) => {
42+
content.append(&mut inlines);
43+
}
44+
PandocNativeIntermediate::IntermediateBaseText(text, range) => {
45+
if let Some(_) = whitespace_re.find(&text) {
46+
content.push(Inline::Space(Space {
47+
source_info: SourceInfo::new(
48+
if context.filenames.is_empty() {
49+
None
50+
} else {
51+
Some(0)
52+
},
53+
range,
54+
),
55+
}))
56+
} else {
57+
content.push(Inline::Str(Str {
58+
text: apply_smart_quotes(text),
59+
source_info: SourceInfo::new(
60+
if context.filenames.is_empty() {
61+
None
62+
} else {
63+
Some(0)
64+
},
65+
range,
66+
),
67+
}))
68+
}
69+
}
70+
PandocNativeIntermediate::IntermediateUnknown(_) => {
71+
// Skip unknown nodes (delimiters, etc.)
72+
}
73+
_ => {
74+
writeln!(
75+
buf,
76+
"Warning: Unexpected node type in {}: {:?}",
77+
stringify!($struct_name:lower),
78+
_node_name
79+
)
80+
.unwrap();
81+
}
82+
}
83+
}
84+
85+
PandocNativeIntermediate::IntermediateInline(Inline::$struct_name($struct_name {
86+
attr,
87+
content,
88+
source_info: node_source_info_with_context(node, context),
89+
}))
90+
}
91+
}
92+
};
93+
}
94+
95+
process_editorial_mark!(Insert);
96+
process_editorial_mark!(Delete);
97+
process_editorial_mark!(Highlight);
98+
process_editorial_mark!(EditComment);

crates/quarto-markdown-pandoc/src/pandoc/treesitter_utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod code_fence_content;
1313
pub mod code_span;
1414
pub mod commonmark_attribute;
1515
pub mod document;
16+
pub mod editorial_marks;
1617
pub mod fenced_code_block;
1718
pub mod fenced_div_block;
1819
pub mod image;

0 commit comments

Comments
 (0)