krz/orgo

Lightning fast org-mode static site generator. fast go org-mode static-site-generator

Commit ce3f2623d5

ce3f2623d5c9eede9c2179aa21afcdf1a4fea05e

parent: 809ac9d720

Unregistered key

cmc <hello@cleberg.net> · 2026-08-23 04:39 UTC
committer: <noreply@github.com>

audit: special blocks are supported, not blind spots (#28)

`#+begin_note` was reported twice as a gap: `??? NOTE` in the block census and
`OUT unmodelled block type` in the construct table. Neither is true. Any block
name with no dedicated handling renders as a div carrying that name and holding
parsed org, which is exactly what org's HTML exporter emits for it — verified
against Emacs 30.2, and already covered by the oracle, since
`fixtures/blocks.org` carries a `#+BEGIN_NOTE` and reports 0 unexplained
divergences. `docs/guide/05-org-support.org:96` has documented this all along.

So no block name is out of scope and none is unrecognised. `scope_of_block` and
`KNOWN_BLOCKS` are gone with it.

`VERSE` and `COMMENT` were also mislabelled: both are handled (a comment block
is dropped from the page, as in org) but fell through to "unmodelled block
type". They now have their own names.

cleberg.net's corpus audits at 100.0% coverage, 0 out-of-scope.

Closes #25

Layout: unified · split

docs/guide/09-auditing.org +5 −9
@@ -26,26 +26,22 @@ IN heading 1148 176 blog/2018-11-28-aes-encr
26IN verbatim 1048 130 blog/2018-11-28-aes-encryption.org:79 26IN verbatim 1048 130 blog/2018-11-28-aes-encryption.org:79
27... 27...
28IN table formula (#+TBLFM:) 4 1 blog/2024-08-11-org-mode-features.org:191 28IN table formula (#+TBLFM:) 4 1 blog/2024-08-11-org-mode-features.org:191
29OUT unmodelled block type 1 1 blog/2026-03-03-auditing-aws-s3.org:50 29IN special block 1 1 blog/2026-03-03-auditing-aws-s3.org:50
30 30
31coverage: 9001 in-scope use(s) (100.0%), 1 out-of-scope (0.0%) 31coverage: 9002 in-scope use(s) (100.0%), 0 out-of-scope (0.0%)
32 32
33KEYWORDS 33KEYWORDS
34 SLUG 180 180 blog/2018-11-28-aes-encryption.org:4 34 SLUG 180 180 blog/2018-11-28-aes-encryption.org:4
35 TITLE 180 180 blog/2018-11-28-aes-encryption.org:2 35 TITLE 180 180 blog/2018-11-28-aes-encryption.org:2
36... 36...
37
38BLOCK TYPES
39 SRC 941 123 blog/2018-11-28-cpp-compiler.org:17
40 QUOTE 115 48 blog/2018-11-28-aes-encryption.org:21
41??? NOTE 1 1 blog/2026-03-03-auditing-aws-s3.org:50
42#+END_EXAMPLE 37#+END_EXAMPLE
43 38
44- =IN= is supported; =OUT= is excluded by design and degrades as described in 39- =IN= is supported; =OUT= is excluded by design and degrades as described in
45 [[file:05-org-support.org][Org support]]. 40 [[file:05-org-support.org][Org support]].
46- The *coverage* line is the number to look at first. 41- The *coverage* line is the number to look at first.
47- =???= marks a name orgo does not recognise at all — here =#+begin_note=, which one 42- =???= marks a name orgo does not recognise at all. That is the blind-spot signal —
48 post uses once. That is the blind-spot signal: not "known unsupported", but unknown. 43 not "known unsupported", but unknown — and this corpus has none. Block names never
44 carry it: an unrecognised one is still a special block, and still renders.
49 45
50Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type, 46Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type,
51drawer name and link scheme in the corpus. A =???= in any of them is worth a look. 47drawer name and link scheme in the corpus. A =???= in any of them is worth a look.
src/audit.rs +9 −17
@@ -83,12 +83,6 @@ const KNOWN_KEYWORDS: &[&str] = &[
83 // Leaving them out reported the corpus's most-used keyword as unrecognized. 83 // Leaving them out reported the corpus's most-used keyword as unrecognized.
84 "SLUG", "DRAFT", "TEMPLATE", 84 "SLUG", "DRAFT", "TEMPLATE",
85]; 85];
86/// Blocks with dedicated handling. Any *other* name renders as a special block — a div
87/// with that name holding parsed org — so an unlisted block is a note about what a corpus
88/// contains rather than a construct that will be lost.
89const KNOWN_BLOCKS: &[&str] = &[
90 "SRC", "QUOTE", "EXAMPLE", "CENTER", "EXPORT", "VERSE", "COMMENT",
91];
92const KNOWN_DRAWERS: &[&str] = &["PROPERTIES", "LOGBOOK", "END"]; 86const KNOWN_DRAWERS: &[&str] = &["PROPERTIES", "LOGBOOK", "END"];
93/// Keyword names conventional enough to be worth flagging when they lead a heading. 87/// Keyword names conventional enough to be worth flagging when they lead a heading.
94/// A custom sequence is only *real* if some `#+TODO:` declares it, which the census 88/// A custom sequence is only *real* if some `#+TODO:` declares it, which the census
@@ -107,7 +101,11 @@ impl Audit {
107 pub fn is_known(kind: Census, name: &str) -> bool { 101 pub fn is_known(kind: Census, name: &str) -> bool {
108 let known = match kind { 102 let known = match kind {
109 Census::Keyword => KNOWN_KEYWORDS, 103 Census::Keyword => KNOWN_KEYWORDS,
110 Census::Block => KNOWN_BLOCKS, 104 // Every block name renders, and renders as org renders it: the names in
105 // `block_construct` through dedicated handling, every other name as a special
106 // block — a div carrying the name, holding parsed org, which is exactly what
107 // org's exporter emits. No block name is a blind spot.
108 Census::Block => return true,
111 Census::Drawer => KNOWN_DRAWERS, 109 Census::Drawer => KNOWN_DRAWERS,
112 Census::Scheme => KNOWN_SCHEMES, 110 Census::Scheme => KNOWN_SCHEMES,
113 }; 111 };
@@ -198,7 +196,7 @@ impl Audit {
198 if let Some(rest) = trimmed.to_ascii_uppercase().strip_prefix("#+BEGIN_") { 196 if let Some(rest) = trimmed.to_ascii_uppercase().strip_prefix("#+BEGIN_") {
199 let kind = rest.split_whitespace().next().unwrap_or("").to_string(); 197 let kind = rest.split_whitespace().next().unwrap_or("").to_string();
200 self.count_census(Census::Block, &kind, &at); 198 self.count_census(Census::Block, &kind, &at);
201 self.count(scope_of_block(&kind), block_construct(&kind), &at); 199 self.count(Scope::In, block_construct(&kind), &at);
202 if trimmed.to_ascii_uppercase().contains(":RESULTS") { 200 if trimmed.to_ascii_uppercase().contains(":RESULTS") {
203 self.count(Scope::Out, "babel header args (:results)", &at); 201 self.count(Scope::Out, "babel header args (:results)", &at);
204 } 202 }
@@ -440,14 +438,6 @@ fn is_drawer(trimmed: &str) -> bool {
440 && t.len() > 2 438 && t.len() > 2
441} 439}
442 440
443fn scope_of_block(kind: &str) -> Scope {
444 if KNOWN_BLOCKS.iter().any(|k| k.eq_ignore_ascii_case(kind)) {
445 Scope::In
446 } else {
447 Scope::Out
448 }
449}
450
451fn block_construct(kind: &str) -> &'static str { 441fn block_construct(kind: &str) -> &'static str {
452 match kind.to_ascii_uppercase().as_str() { 442 match kind.to_ascii_uppercase().as_str() {
453 "SRC" => "source block", 443 "SRC" => "source block",
@@ -455,7 +445,9 @@ fn block_construct(kind: &str) -> &'static str {
455 "EXAMPLE" => "example block", 445 "EXAMPLE" => "example block",
456 "CENTER" => "center block", 446 "CENTER" => "center block",
457 "EXPORT" => "export block", 447 "EXPORT" => "export block",
458 _ => "unmodelled block type", 448 "VERSE" => "verse block",
449 "COMMENT" => "comment block",
450 _ => "special block",
459 } 451 }
460} 452}
461 453
tests/constructs.rs +21
@@ -816,3 +816,24 @@ fn audit_counts_table_formulas_as_in_scope() {
816 "a #+TBLFM: table orgo renders exactly as Emacs does must not read as a gap:\n{line}" 816 "a #+TBLFM: table orgo renders exactly as Emacs does must not read as a gap:\n{line}"
817 ); 817 );
818} 818}
819
820/// A block name with no dedicated handling is a special block, which org itself renders
821/// as a div carrying the name — `fixtures/blocks.org` holds `#+BEGIN_NOTE` and is in the
822/// Emacs oracle. So it is neither out of scope nor a `???` blind spot.
823#[test]
824fn audit_counts_special_blocks_as_in_scope() {
825 let report = audit_fixture("blocks.org");
826 let line = report
827 .lines()
828 .find(|l| l.contains("special block"))
829 .expect("the NOTE block is counted as a special block");
830 assert!(
831 line.starts_with("IN "),
832 "a block orgo renders exactly as Emacs does must not read as a gap:\n{line}"
833 );
834 let flagged: Vec<&str> = report.lines().filter(|l| l.starts_with("???")).collect();
835 assert!(
836 flagged.is_empty(),
837 "no block name is unrecognised — every one renders: {flagged:?}"
838 );
839}