krz/orgo

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

Commit 82639e554b

82639e554bcf4a7465c41e6c47effceb501d91d3

parent: 54a241c7fe

Unregistered key

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

audit: count only entity names org knows (#26)

`entity_ref` matched any backslash followed by three or more letters, so
`C:\Users\youruser\Downloads` and `Tumblr\API\Client` were reported as org
entity references. It now consults `entities::lookup` over the full alphabetic
run, which is the rule `parser::try_entity` applies when deciding whether to
render one.

Inline `=verbatim=` and `~code~` spans are blanked before the scan. `=\alpha=`
shows the name rather than rendering the character, so it is not a use of the
feature — source blocks were already skipped for the same reason.

cleberg.net's content drops from 8 out-of-scope uses to 5, and orgo's own docs
from 1 entity hit to none; both corpora contain no real entities.

Closes #23

Layout: unified · split

fixtures/audit-entities.org added +3
@@ -0,0 +1,3 @@
1#+TITLE: Real entities
2
3The angle \alpha is small, and the arrow \rarr points right.
fixtures/audit-nonentities.org added +7
@@ -0,0 +1,7 @@
1#+TITLE: Backslashes that are not entities
2
3Windows paths and namespaced identifiers are not org entities:
4=C:\Users\youruser\Downloads= and Tumblr\API\Client.
5
6Names shown inside verbatim or code are displayed, not rendered, so they are
7not a use of the feature either: =\alpha=, ~\rarr~, =20\deg=.
src/audit.rs +38 −6
@@ -506,21 +506,53 @@ fn latex_inline(line: &str) -> bool {
506506 dollars >= 2 && line.contains("$\\")
507507}
508508
509/// A `\name` entity reference such as `\alpha`, excluding LaTeX environment commands.
509/// A `\name` entity reference such as `\alpha`.
510///
511/// Only names org knows count, matching [`crate::parser`]'s rule for rendering one: a
512/// Windows path (`C:\Users\me`) and a namespaced identifier (`Tumblr\API\Client`) are not
513/// entity references.
514///
515/// Verbatim and code spans are skipped: `=\alpha=` shows the name rather than rendering
516/// the character, so it is not a use of the feature.
510517fn entity_ref(line: &str) -> bool {
511 for (i, c) in line.char_indices() {
512 if c != '\\' {
518 let line = without_literal_spans(line);
519 let chars: Vec<char> = line.chars().collect();
520 for (i, c) in chars.iter().enumerate() {
521 if *c != '\\' {
513522 continue;
514523 }
515 let rest = &line[i + 1..];
516 let name: String = rest.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
517 if name.len() >= 3 && !matches!(name.as_str(), "begin" | "end") {
524 let name: String = chars[i + 1..].iter().take_while(|c| c.is_ascii_alphabetic()).collect();
525 if crate::entities::lookup(&name).is_some() {
518526 return true;
519527 }
520528 }
521529 false
522530}
523531
532/// Blank out `=verbatim=` and `~code~` spans. Deliberately looser than the parser's
533/// border rules — the audit measures prevalence, and erring toward blanking keeps it
534/// from overstating a gap.
535fn without_literal_spans(line: &str) -> String {
536 let mut out = String::with_capacity(line.len());
537 let mut open: Option<char> = None;
538 for c in line.chars() {
539 match open {
540 Some(marker) => {
541 out.push(' ');
542 if c == marker {
543 open = None;
544 }
545 }
546 None if c == '=' || c == '~' => {
547 open = Some(c);
548 out.push(' ');
549 }
550 None => out.push(c),
551 }
552 }
553 out
554}
555
524556/// A plausible `*bold*`-style emphasis pair: two markers on one line with non-space
525557/// content between them. Approximate by design — the audit measures prevalence, and the
526558/// parser owns the exact pre/post-character rules.
tests/constructs.rs +31
@@ -769,3 +769,34 @@ fn footnote_links_and_section_are_labelled() {
769769 "each back-link says where it goes:\n{html}"
770770 );
771771}
772
773// ---------------------------------------------------------------------------
774// Audit: the entity check counts only what org would actually render
775// ---------------------------------------------------------------------------
776
777fn audit_fixture(name: &str) -> String {
778 let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures").join(name);
779 let audit = orgo::audit::audit(&path).expect("audit fixture");
780 orgo::audit::report(&audit)
781}
782
783/// `\Users` and `\API` are not names org knows, and an entity inside verbatim is shown
784/// rather than rendered. Counting either overstates what the corpus needs.
785#[test]
786fn audit_ignores_backslashes_that_are_not_entities() {
787 let report = audit_fixture("audit-nonentities.org");
788 assert!(
789 !report.contains("entity (\\name)"),
790 "a Windows path or a verbatim-quoted name was counted as an entity:\n{report}"
791 );
792}
793
794/// A bare entity outside verbatim still counts.
795#[test]
796fn audit_counts_real_entities() {
797 let report = audit_fixture("audit-entities.org");
798 assert!(
799 report.contains("entity (\\name)"),
800 "a real entity was not counted:\n{report}"
801 );
802}