fix(html): emit heading id anchors so exported #links resolve
Exported HTML anchor links (TOC entries, [[Page#Heading]], [[#Heading]]) went nowhere because heading elements carried no `id`. Worse, the two TOC builders slugified anchors (`#my-heading`) while the link resolver emits the raw heading text (`#My Heading`, matching upstream vimwiki), so even once ids existed the two halves wouldn't have agreed. Fix — adopt vimwiki's scheme (raw heading text as the anchor) everywhere: - render_heading emits `<hN id="<plain heading text>">`. - build_toc_html (HTML export TOC) uses the raw, HTML-escaped title for both the href and the link text (was slugify). - collect_toc_items (:VimwikiTOC buffer output) uses the raw title for the generated `[[#anchor]]` (was slugify). In-editor navigation slugifies both sides, so it still resolves. Consistency: add canonical `nuwiki_core::ast::inline_text()` and route the heading id, the HTML-TOC title, the buffer-TOC title, and `diagnostics::heading_text` through it — three duplicated extractors collapsed into one, so the anchor and its validation can't drift. Regression test: heading_id_matches_anchor_link_href (core) asserts the heading id and a `#anchor` link's href are identical. Updated the heading assertions in the renderer/export tests for the new `id=` attribute. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,46 @@ impl InlineNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concatenate the plain-text content of a run of inlines, descending into
|
||||||
|
/// formatting containers. Links/external links use their description, falling
|
||||||
|
/// back to the target path / URL. This is the canonical heading/anchor text —
|
||||||
|
/// the HTML renderer uses it for heading `id`s and the LSP for anchor matching,
|
||||||
|
/// so the two never drift.
|
||||||
|
pub fn inline_text(inlines: &[InlineNode]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
push_inline_text(inlines, &mut out);
|
||||||
|
out.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_inline_text(inlines: &[InlineNode], out: &mut String) {
|
||||||
|
for n in inlines {
|
||||||
|
match n {
|
||||||
|
InlineNode::Text(t) => out.push_str(&t.content),
|
||||||
|
InlineNode::Bold(b) => push_inline_text(&b.children, out),
|
||||||
|
InlineNode::Italic(i) => push_inline_text(&i.children, out),
|
||||||
|
InlineNode::BoldItalic(bi) => push_inline_text(&bi.children, out),
|
||||||
|
InlineNode::Strikethrough(s) => push_inline_text(&s.children, out),
|
||||||
|
InlineNode::Code(c) => out.push_str(&c.content),
|
||||||
|
InlineNode::Superscript(s) => push_inline_text(&s.children, out),
|
||||||
|
InlineNode::Subscript(s) => push_inline_text(&s.children, out),
|
||||||
|
InlineNode::Color(c) => push_inline_text(&c.children, out),
|
||||||
|
InlineNode::WikiLink(w) => match &w.description {
|
||||||
|
Some(d) => push_inline_text(d, out),
|
||||||
|
None => {
|
||||||
|
if let Some(p) = &w.target.path {
|
||||||
|
out.push_str(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
InlineNode::ExternalLink(e) => match &e.description {
|
||||||
|
Some(d) => push_inline_text(d, out),
|
||||||
|
None => out.push_str(&e.url),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TextNode {
|
pub struct TextNode {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ pub use block::{
|
|||||||
TagScope,
|
TagScope,
|
||||||
};
|
};
|
||||||
pub use inline::{
|
pub use inline::{
|
||||||
BoldItalicNode, BoldNode, CodeNode, ColorNode, InlineNode, ItalicNode, Keyword, KeywordNode,
|
inline_text, BoldItalicNode, BoldNode, CodeNode, ColorNode, InlineNode, ItalicNode, Keyword,
|
||||||
MathInlineNode, SoftBreakNode, StrikethroughNode, SubscriptNode, SuperscriptNode, TextNode,
|
KeywordNode, MathInlineNode, SoftBreakNode, StrikethroughNode, SubscriptNode, SuperscriptNode,
|
||||||
|
TextNode,
|
||||||
};
|
};
|
||||||
pub use link::{
|
pub use link::{
|
||||||
ExternalLinkNode, LinkKind, LinkTarget, RawUrlNode, TransclusionNode, WikiLinkNode,
|
ExternalLinkNode, LinkKind, LinkTarget, RawUrlNode, TransclusionNode, WikiLinkNode,
|
||||||
|
|||||||
@@ -319,7 +319,13 @@ impl HtmlRenderer {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
write!(w, "<h{level}{class}>")?;
|
// `id` is the heading's plain text (vimwiki's anchor scheme), so
|
||||||
|
// `[[Page#Heading]]` and TOC `#Heading` links land here. Matches the
|
||||||
|
// anchor text the LSP validates and the resolver emits.
|
||||||
|
let anchor = crate::ast::inline_text(&n.children);
|
||||||
|
write!(w, "<h{level}{class} id=\"")?;
|
||||||
|
write_escaped(&anchor, w)?;
|
||||||
|
write!(w, "\">")?;
|
||||||
if !number.is_empty() {
|
if !number.is_empty() {
|
||||||
write_escaped(number, w)?;
|
write_escaped(number, w)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,14 +63,14 @@ fn heading_levels() {
|
|||||||
for level in 1..=6 {
|
for level in 1..=6 {
|
||||||
let bars = "=".repeat(level);
|
let bars = "=".repeat(level);
|
||||||
let out = render(&format!("{bars} Title {bars}\n"));
|
let out = render(&format!("{bars} Title {bars}\n"));
|
||||||
assert!(out.starts_with(&format!("<h{level}>Title</h{level}>")));
|
assert!(out.starts_with(&format!("<h{level} id=\"Title\">Title</h{level}>")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn centered_heading_gets_centered_class() {
|
fn centered_heading_gets_centered_class() {
|
||||||
let out = render(" = T =\n");
|
let out = render(" = T =\n");
|
||||||
assert!(out.contains("<h1 class=\"centered\">T</h1>"));
|
assert!(out.contains("<h1 class=\"centered\" id=\"T\">T</h1>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -237,6 +237,22 @@ fn anchor_only_link() {
|
|||||||
assert!(out.contains("<a href=\"#Section\">"));
|
assert!(out.contains("<a href=\"#Section\">"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heading_id_matches_anchor_link_href() {
|
||||||
|
// The heading carries an `id` equal to its plain text, so a `#anchor`
|
||||||
|
// link (TOC or cross-reference) actually lands on it. Regression guard
|
||||||
|
// for "exported HTML anchor links don't work".
|
||||||
|
let out = render("= My Section =\n\n[[#My Section]]\n");
|
||||||
|
assert!(
|
||||||
|
out.contains("<h1 id=\"My Section\">My Section</h1>"),
|
||||||
|
"heading id: {out}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.contains("<a href=\"#My Section\">"),
|
||||||
|
"anchor href: {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interwiki_numbered_link() {
|
fn interwiki_numbered_link() {
|
||||||
let out = render("[[wiki1:Page]]\n");
|
let out = render("[[wiki1:Page]]\n");
|
||||||
@@ -299,7 +315,7 @@ fn template_substitutes_content_and_title() {
|
|||||||
);
|
);
|
||||||
let out = renderer.render_to_string(&doc).unwrap();
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
assert!(out.contains("<title>My Page</title>"));
|
assert!(out.contains("<title>My Page</title>"));
|
||||||
assert!(out.contains("<body><h1>Hello</h1>"));
|
assert!(out.contains("<body><h1 id=\"Hello\">Hello</h1>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -308,7 +324,7 @@ fn template_with_missing_title_substitutes_empty_string() {
|
|||||||
let renderer = HtmlRenderer::new().with_template("<title>{{title}}</title>{{content}}");
|
let renderer = HtmlRenderer::new().with_template("<title>{{title}}</title>{{content}}");
|
||||||
let out = renderer.render_to_string(&doc).unwrap();
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
assert!(out.contains("<title></title>"));
|
assert!(out.contains("<title></title>"));
|
||||||
assert!(out.contains("<h1>Heading</h1>"));
|
assert!(out.contains("<h1 id=\"Heading\">Heading</h1>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -323,7 +339,10 @@ fn template_substitutes_vimwiki_percent_placeholders() {
|
|||||||
.with_var("root_path", "../");
|
.with_var("root_path", "../");
|
||||||
let out = renderer.render_to_string(&doc).unwrap();
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
assert!(out.contains("<title>My Page</title>"), "title: {out}");
|
assert!(out.contains("<title>My Page</title>"), "title: {out}");
|
||||||
assert!(out.contains("<body><h1>Hello</h1>"), "content: {out}");
|
assert!(
|
||||||
|
out.contains("<body><h1 id=\"Hello\">Hello</h1>"),
|
||||||
|
"content: {out}"
|
||||||
|
);
|
||||||
assert!(out.contains("href=\"../style.css\""), "root_path: {out}");
|
assert!(out.contains("href=\"../style.css\""), "root_path: {out}");
|
||||||
assert!(!out.contains('%'), "no placeholder left: {out}");
|
assert!(!out.contains('%'), "no placeholder left: {out}");
|
||||||
}
|
}
|
||||||
@@ -415,7 +434,7 @@ fn x() {}
|
|||||||
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
||||||
// A few sanity checks; the full output is exercised piece-by-piece above.
|
// A few sanity checks; the full output is exercised piece-by-piece above.
|
||||||
for needle in [
|
for needle in [
|
||||||
"<h1>Heading</h1>",
|
"<h1 id=\"Heading\">Heading</h1>",
|
||||||
"<strong>bold</strong>",
|
"<strong>bold</strong>",
|
||||||
"<em>italic</em>",
|
"<em>italic</em>",
|
||||||
"<code>code</code>",
|
"<code>code</code>",
|
||||||
|
|||||||
@@ -2044,11 +2044,14 @@ pub mod ops {
|
|||||||
// skip the TOC's own heading
|
// skip the TOC's own heading
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let anchor = crate::index::slugify(&title);
|
// Anchor is the raw heading text (vimwiki scheme) so the generated
|
||||||
|
// `[[#anchor]]` link matches the heading `id` the HTML renderer
|
||||||
|
// emits on export. In-editor navigation slugifies both sides, so it
|
||||||
|
// resolves regardless.
|
||||||
out.push(TocItem {
|
out.push(TocItem {
|
||||||
level: h.level,
|
level: h.level,
|
||||||
|
anchor: title.clone(),
|
||||||
title,
|
title,
|
||||||
anchor,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|||||||
@@ -270,40 +270,11 @@ pub fn classify_outgoing(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Flatten heading inlines down to a plain string. Used by `commands::ops`
|
/// Flatten heading inlines down to a plain string. Used by `commands::ops`
|
||||||
/// to match heading text against the configured TOC/Links section name.
|
/// to match heading text against the configured TOC/Links section name, and
|
||||||
|
/// (via `nuwiki_core::ast::inline_text`) the same text the HTML renderer uses
|
||||||
|
/// for heading `id`s — so anchors validated here match anchors emitted there.
|
||||||
pub fn heading_text(inlines: &[InlineNode]) -> String {
|
pub fn heading_text(inlines: &[InlineNode]) -> String {
|
||||||
let mut s = String::new();
|
nuwiki_core::ast::inline_text(inlines)
|
||||||
push_inline_text(inlines, &mut s);
|
|
||||||
s.trim().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_inline_text(inlines: &[InlineNode], out: &mut String) {
|
|
||||||
for n in inlines {
|
|
||||||
match n {
|
|
||||||
InlineNode::Text(t) => out.push_str(&t.content),
|
|
||||||
InlineNode::Bold(b) => push_inline_text(&b.children, out),
|
|
||||||
InlineNode::Italic(i) => push_inline_text(&i.children, out),
|
|
||||||
InlineNode::BoldItalic(bi) => push_inline_text(&bi.children, out),
|
|
||||||
InlineNode::Strikethrough(s) => push_inline_text(&s.children, out),
|
|
||||||
InlineNode::Code(c) => out.push_str(&c.content),
|
|
||||||
InlineNode::Superscript(s) => push_inline_text(&s.children, out),
|
|
||||||
InlineNode::Subscript(s) => push_inline_text(&s.children, out),
|
|
||||||
InlineNode::Color(c) => push_inline_text(&c.children, out),
|
|
||||||
InlineNode::WikiLink(w) => match &w.description {
|
|
||||||
Some(d) => push_inline_text(d, out),
|
|
||||||
None => {
|
|
||||||
if let Some(p) = &w.target.path {
|
|
||||||
out.push_str(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
InlineNode::ExternalLink(e) => match &e.description {
|
|
||||||
Some(d) => push_inline_text(d, out),
|
|
||||||
None => out.push_str(&e.url),
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a `file:` / `local:` link target to an absolute path.
|
/// Resolve a `file:` / `local:` link target to an absolute path.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use nuwiki_core::ast::{BlockNode, DocumentNode, HeadingNode, InlineNode, LinkKind};
|
use nuwiki_core::ast::{BlockNode, DocumentNode, LinkKind};
|
||||||
use nuwiki_core::date::DiaryDate;
|
use nuwiki_core::date::DiaryDate;
|
||||||
use nuwiki_core::render::{HtmlRenderer, Renderer};
|
use nuwiki_core::render::{HtmlRenderer, Renderer};
|
||||||
|
|
||||||
@@ -147,11 +147,10 @@ pub fn build_toc_html(doc: &DocumentNode) -> String {
|
|||||||
out.push_str("</ul>");
|
out.push_str("</ul>");
|
||||||
depth -= 1;
|
depth -= 1;
|
||||||
}
|
}
|
||||||
let anchor = crate::index::slugify(&h.title);
|
// Anchor is the raw heading text (vimwiki scheme), HTML-escaped — must
|
||||||
out.push_str(&format!(
|
// match the `id` the renderer puts on the heading element.
|
||||||
"<li><a href=\"#{anchor}\">{title}</a></li>",
|
let anchor = escape_html(&h.title);
|
||||||
title = escape_html(&h.title)
|
out.push_str(&format!("<li><a href=\"#{anchor}\">{anchor}</a></li>"));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
while depth > 0 {
|
while depth > 0 {
|
||||||
out.push_str("</ul>");
|
out.push_str("</ul>");
|
||||||
@@ -300,7 +299,7 @@ fn collect_headings(doc: &DocumentNode) -> Vec<HeadingProj> {
|
|||||||
if let BlockNode::Heading(h) = b {
|
if let BlockNode::Heading(h) = b {
|
||||||
out.push(HeadingProj {
|
out.push(HeadingProj {
|
||||||
level: h.level,
|
level: h.level,
|
||||||
title: inline_to_text(h),
|
title: nuwiki_core::ast::inline_text(&h.children),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,41 +311,6 @@ struct HeadingProj {
|
|||||||
title: String,
|
title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inline_to_text(h: &HeadingNode) -> String {
|
|
||||||
let mut s = String::new();
|
|
||||||
for n in &h.children {
|
|
||||||
push_inline(n, &mut s);
|
|
||||||
}
|
|
||||||
s.trim().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_inline(n: &InlineNode, out: &mut String) {
|
|
||||||
match n {
|
|
||||||
InlineNode::Text(t) => out.push_str(&t.content),
|
|
||||||
InlineNode::Bold(b) => b.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Italic(i) => i.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::BoldItalic(bi) => bi.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Strikethrough(s) => s.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Code(c) => out.push_str(&c.content),
|
|
||||||
InlineNode::Superscript(s) => s.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Subscript(s) => s.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Color(c) => c.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::WikiLink(w) => match &w.description {
|
|
||||||
Some(d) => d.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
None => {
|
|
||||||
if let Some(p) = &w.target.path {
|
|
||||||
out.push_str(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
InlineNode::ExternalLink(e) => match &e.description {
|
|
||||||
Some(d) => d.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
None => out.push_str(&e.url),
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escape_html(s: &str) -> String {
|
fn escape_html(s: &str) -> String {
|
||||||
let mut out = String::with_capacity(s.len());
|
let mut out = String::with_capacity(s.len());
|
||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
|
|||||||
@@ -384,10 +384,22 @@ fn render_page_html_numbers_headers_when_enabled() {
|
|||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(html.contains("<h1>1. Intro</h1>"), "got: {html}");
|
assert!(
|
||||||
assert!(html.contains("<h2>1.1. Background</h2>"), "got: {html}");
|
html.contains("<h1 id=\"Intro\">1. Intro</h1>"),
|
||||||
assert!(html.contains("<h2>1.2. Methods</h2>"), "got: {html}");
|
"got: {html}"
|
||||||
assert!(html.contains("<h1>2. Results</h1>"), "got: {html}");
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains("<h2 id=\"Background\">1.1. Background</h2>"),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains("<h2 id=\"Methods\">1.2. Methods</h2>"),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains("<h1 id=\"Results\">2. Results</h1>"),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -407,11 +419,14 @@ fn render_page_html_numbering_skips_headers_above_start_level() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
html.contains("<h1>Title</h1>"),
|
html.contains("<h1 id=\"Title\">Title</h1>"),
|
||||||
"h1 unnumbered, got: {html}"
|
"h1 unnumbered, got: {html}"
|
||||||
);
|
);
|
||||||
assert!(html.contains("<h2>1 Section</h2>"), "got: {html}");
|
assert!(
|
||||||
assert!(html.contains("<h3>1.1 Sub</h3>"), "got: {html}");
|
html.contains("<h2 id=\"Section\">1 Section</h2>"),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(html.contains("<h3 id=\"Sub\">1.1 Sub</h3>"), "got: {html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -428,8 +443,8 @@ fn render_page_html_no_header_numbering_by_default() {
|
|||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(html.contains("<h1>Intro</h1>"), "got: {html}");
|
assert!(html.contains("<h1 id=\"Intro\">Intro</h1>"), "got: {html}");
|
||||||
assert!(html.contains("<h2>Sub</h2>"), "got: {html}");
|
assert!(html.contains("<h2 id=\"Sub\">Sub</h2>"), "got: {html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -492,7 +492,9 @@ fn toc_edit_inserts_at_top_when_no_existing_toc() {
|
|||||||
// Insertion at position 0,0 → zero-width range.
|
// Insertion at position 0,0 → zero-width range.
|
||||||
assert_eq!(te.range.start, te.range.end);
|
assert_eq!(te.range.start, te.range.end);
|
||||||
assert!(te.new_text.starts_with("= Contents =\n"));
|
assert!(te.new_text.starts_with("= Contents =\n"));
|
||||||
assert!(te.new_text.contains("[[#one|One]]"));
|
// Anchor is the raw heading text (vimwiki scheme), matching the heading id
|
||||||
|
// the HTML renderer emits on export.
|
||||||
|
assert!(te.new_text.contains("[[#One|One]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -504,7 +506,7 @@ fn toc_edit_replaces_existing_toc() {
|
|||||||
let changes = edit.changes.expect("changes map");
|
let changes = edit.changes.expect("changes map");
|
||||||
let edits = &changes[&uri];
|
let edits = &changes[&uri];
|
||||||
let te = &edits[0];
|
let te = &edits[0];
|
||||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
assert!(te.new_text.contains("[[#Real|Real]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
// The replacement range must start at line 0.
|
// The replacement range must start at line 0.
|
||||||
assert_eq!(te.range.start.line, 0);
|
assert_eq!(te.range.start.line, 0);
|
||||||
@@ -536,7 +538,7 @@ fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
|||||||
let edit =
|
let edit =
|
||||||
ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1, 0, 0).expect("rebuild edit");
|
ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1, 0, 0).expect("rebuild edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
assert!(te.new_text.contains("[[#Real|Real]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user