Expand description
Selecting part of a note the way an Obsidian link fragment does:
[[Note#Heading]], [[Note#Parent#Child]] and [[Note#^block-id]].
This reads the body as lines, not through the crate::DocTree
projection, because the two answer different questions. The tree makes
the first H1 the document title, while a link fragment treats every
heading the same: #Title selects the H1 section like any other heading.
The rules are Obsidian’s:
- A heading is an ATX heading (
#to######, then whitespace, then text), up to three spaces in. A closing#run counts only after a space or tab, so## Learn C#is the headingLearn C#. Headings inside fenced code are not headings (fences asCodeRules::SkillExportreads them). - A section is its heading line through the line before the next heading of the same or a shallower level, so subsections come with it.
- A heading path (
Parent#Child) names the heading by its last segment; the segments before it must be the nearest headings above it, in order (plain heading ancestry: an H1 is an ancestor like any other heading). The first heading that matches wins. - A selector is first tried as one heading’s whole text, since a
heading may contain
#; only when no heading has that text is it split on#into a path. - A block
^idis labelled by a marker at the end of a line (after whitespace) or alone on a line. Alone on its line, it labels the paragraph or list above it, blank lines skipped. At the end of a list item it labels that one item. At the end of any other line it labels the paragraph the line ends, walked back to a blank line or a heading. The marker is never part of the selection.
Every selection is trimmed of surrounding whitespace. The *_range
functions give the selection’s bytes in the body; the others give the text,
which is the same bytes with each \r\n read as \n.
The same rules back crate::DocTree::section_by_heading_path,
crate::DocTree::section_by_selector and
crate::DocTree::block_range_by_id.
use markdown_contract::{parse_document, select};
let note = "---\ntitle: x\n---\n# Guide\n\n## Setup\n\nInstall it. ^install\n\n## Use\n";
let tree = parse_document(note);
let range = tree.section_by_heading_path(&["Guide", "Setup"]).unwrap();
assert_eq!(&tree.body[range], "## Setup\n\nInstall it. ^install");
assert_eq!(select::block(&tree.body, "install").as_deref(), Some("Install it."));
assert_eq!(
tree.block_by_id("install").unwrap().range(),
tree.root.sections[0].blocks[0].range()
);Functions§
- atx_
heading - An ATX heading’s level and text:
^ {0,3}(#{1,6})\s+(.+?)\s*$, with a closing#run stripped when a space or tab precedes it. A bare##or##is not a heading;##(whitespace for text) is one with empty text, which ends sections but can never be selected. - block
block_rangeas text.- block_
range - The block
^idlabels; see the module doc for the three shapes. - fragment
fragment_rangeas text.- fragment_
range - What a link fragment selects: no fragment the whole body,
^ida block, anything else a heading selector (heading_range). All trimmed. - heading
heading_rangeas text.- heading_
path heading_path_rangeas text.- heading_
path_ range - The section of the first heading whose text is
path’s last segment and whose nearest ancestors are the segments before it. Segments are compared exactly, as given; an emptypathselects nothing. - heading_
range - The section a link fragment’s heading selector names: the whole trimmed
selector as one heading’s text first, then the selector split on
#(empty segments dropped, each trimmed) as a heading path.