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 | |
| 3 | The 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 | |
| 3 | Windows paths and namespaced identifiers are not org entities: |
| 4 | =C:\Users\youruser\Downloads= and Tumblr\API\Client. |
| 5 | |
| 6 | Names shown inside verbatim or code are displayed, not rendered, so they are |
| 7 | not 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 { |
| 506 | 506 | dollars >= 2 && line.contains("$\\") |
| 507 | 507 | } |
| 508 | 508 | |
| 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. |
| 510 | 517 | fn 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 != '\\' { |
| 513 | 522 | continue; |
| 514 | 523 | } |
| 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() { |
| 518 | 526 | return true; |
| 519 | 527 | } |
| 520 | 528 | } |
| 521 | 529 | false |
| 522 | 530 | } |
| 523 | 531 | |
| 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. |
| 535 | fn 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 | |
| 524 | 556 | /// A plausible `*bold*`-style emphasis pair: two markers on one line with non-space |
| 525 | 557 | /// content between them. Approximate by design — the audit measures prevalence, and the |
| 526 | 558 | /// parser owns the exact pre/post-character rules. |
tests/constructs.rs
+31
| @@ -769,3 +769,34 @@ fn footnote_links_and_section_are_labelled() { |
| 769 | 769 | "each back-link says where it goes:\n{html}" |
| 770 | 770 | ); |
| 771 | 771 | } |
| 772 | |
| 773 | // --------------------------------------------------------------------------- |
| 774 | // Audit: the entity check counts only what org would actually render |
| 775 | // --------------------------------------------------------------------------- |
| 776 | |
| 777 | fn 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] |
| 786 | fn 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] |
| 796 | fn 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 | } |