new: support links referencing headings
Previously, links referencing a heading (`[[note#heading]]`) would just link to the file name without including an anchor in the link target. Now, such references will include an appropriate `#anchor` attribute. Note that neither the original Markdown specification, nor the more recent CommonMark standard, specify how anchors should be constructed for a given heading. There are also some differences between the various Markdown rendering implementations. Obsidian-export uses the [slug] crate to generate anchors which should be compatible with most implementations, however your mileage may vary. (For example, GitHub may leave a trailing `-` on anchors when headings end with a smiley. The slug library, and thus obsidian-export, will avoid such dangling dashes). [slug]: https://crates.io/crates/slug
This commit is contained in:
parent
fcb4cd9dec
commit
6033407266
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -113,6 +113,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deunicode"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
|
||||
|
||||
[[package]]
|
||||
name = "difference"
|
||||
version = "2.0.0"
|
||||
@ -305,6 +311,7 @@ dependencies = [
|
||||
"pulldown-cmark-to-cmark",
|
||||
"rayon",
|
||||
"regex",
|
||||
"slug",
|
||||
"snafu",
|
||||
"tempfile",
|
||||
"walkdir",
|
||||
@ -508,6 +515,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "slug"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
|
||||
dependencies = [
|
||||
"deunicode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snafu"
|
||||
version = "0.6.10"
|
||||
|
@ -35,6 +35,7 @@ pulldown-cmark = "0.8.0"
|
||||
pulldown-cmark-to-cmark = "6.0.0"
|
||||
rayon = "1.5.0"
|
||||
regex = "1.4.2"
|
||||
slug = "0.1.4"
|
||||
snafu = "0.6.10"
|
||||
|
||||
[dev-dependencies]
|
||||
|
68
src/lib.rs
68
src/lib.rs
@ -11,8 +11,10 @@ use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::cmark_with_options;
|
||||
use rayon::prelude::*;
|
||||
use regex::Regex;
|
||||
use slug::slugify;
|
||||
use snafu::{ResultExt, Snafu};
|
||||
use std::ffi::OsString;
|
||||
use std::fmt;
|
||||
use std::fs::{self, File};
|
||||
use std::io::prelude::*;
|
||||
use std::io::ErrorKind;
|
||||
@ -196,6 +198,24 @@ impl<'a> ObsidianNoteReference<'a> {
|
||||
section,
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
format!("{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for ObsidianNoteReference<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = self
|
||||
.label
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| match self.section {
|
||||
Some(section) => format!("{} > {}", self.file, section),
|
||||
None => self.file.to_string(),
|
||||
})
|
||||
.to_string();
|
||||
write!(f, "{}", label)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Exporter<'a> {
|
||||
@ -384,9 +404,11 @@ impl<'a> Exporter<'a> {
|
||||
if let Event::Text(CowStr::Borrowed(text)) = buffer[2] {
|
||||
match buffer[0] {
|
||||
Event::Text(CowStr::Borrowed("[")) => {
|
||||
let mut link_events =
|
||||
self.obsidian_note_link_to_markdown(&text, context);
|
||||
tree.append(&mut link_events);
|
||||
let mut elements = self.make_link_to_file(
|
||||
ObsidianNoteReference::from_str(&text),
|
||||
context,
|
||||
);
|
||||
tree.append(&mut elements);
|
||||
buffer.clear();
|
||||
continue;
|
||||
}
|
||||
@ -445,7 +467,7 @@ impl<'a> Exporter<'a> {
|
||||
tree
|
||||
}
|
||||
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("webp") => {
|
||||
self.make_link_to_file(¬e_ref.file, ¬e_ref.file, &context)
|
||||
self.make_link_to_file(note_ref, &context)
|
||||
.into_iter()
|
||||
.map(|event| match event {
|
||||
// make_link_to_file returns a link to a file. With this we turn the link
|
||||
@ -470,34 +492,28 @@ impl<'a> Exporter<'a> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => self.make_link_to_file(¬e_ref.file, ¬e_ref.file, &context),
|
||||
_ => self.make_link_to_file(note_ref, &context),
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
fn obsidian_note_link_to_markdown(&self, content: &'a str, context: &Context) -> MarkdownTree {
|
||||
let note_ref = ObsidianNoteReference::from_str(content);
|
||||
let label = note_ref.label.unwrap_or(note_ref.file);
|
||||
self.make_link_to_file(note_ref.file, label, context)
|
||||
}
|
||||
|
||||
fn make_link_to_file<'b>(
|
||||
&self,
|
||||
file: &'b str,
|
||||
label: &'b str,
|
||||
reference: ObsidianNoteReference<'b>,
|
||||
context: &Context,
|
||||
) -> MarkdownTree<'b> {
|
||||
let target_file = lookup_filename_in_vault(file, &self.vault_contents.as_ref().unwrap());
|
||||
let target_file =
|
||||
lookup_filename_in_vault(reference.file, &self.vault_contents.as_ref().unwrap());
|
||||
if target_file.is_none() {
|
||||
// TODO: Extract into configurable function.
|
||||
println!(
|
||||
"Warning: Unable to find referenced note\n\tReference: '{}'\n\tSource: '{}'\n",
|
||||
file,
|
||||
reference.file,
|
||||
context.current_file().display(),
|
||||
);
|
||||
return vec![
|
||||
Event::Start(Tag::Emphasis),
|
||||
Event::Text(CowStr::from(String::from(label))),
|
||||
Event::Text(CowStr::from(reference.display())),
|
||||
Event::End(Tag::Emphasis),
|
||||
];
|
||||
}
|
||||
@ -513,19 +529,25 @@ impl<'a> Exporter<'a> {
|
||||
.expect("obsidian content files should always have a parent"),
|
||||
)
|
||||
.expect("should be able to build relative path when target file is found in vault");
|
||||
let rel_link = rel_link.to_string_lossy();
|
||||
let encoded_link = utf8_percent_encode(&rel_link, PERCENTENCODE_CHARS);
|
||||
|
||||
let link = pulldown_cmark::Tag::Link(
|
||||
let rel_link = rel_link.to_string_lossy();
|
||||
let mut link = utf8_percent_encode(&rel_link, PERCENTENCODE_CHARS).to_string();
|
||||
|
||||
if let Some(section) = reference.section {
|
||||
link.push('#');
|
||||
link.push_str(&slugify(section));
|
||||
}
|
||||
|
||||
let link_tag = pulldown_cmark::Tag::Link(
|
||||
pulldown_cmark::LinkType::Inline,
|
||||
CowStr::from(encoded_link.to_string()),
|
||||
CowStr::from(link.to_string()),
|
||||
CowStr::from(""),
|
||||
);
|
||||
|
||||
vec![
|
||||
Event::Start(link.clone()),
|
||||
Event::Text(CowStr::from(label)),
|
||||
Event::End(link.clone()),
|
||||
Event::Start(link_tag.clone()),
|
||||
Event::Text(CowStr::from(reference.display())),
|
||||
Event::End(link_tag.clone()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ Link to [pure-markdown-examples](pure-markdown-examples.md) and the same [Pure-M
|
||||
|
||||
Link to [pure markdown examples](pure-markdown-examples.md).
|
||||
|
||||
Link to [pure-markdown-examples > Heading 1](pure-markdown-examples.md#heading-1).
|
||||
|
||||
Link to [pure markdown examples](pure-markdown-examples.md#heading-1).
|
||||
|
||||
Link within backticks: `[[pure-markdown-examples]]`
|
||||
|
||||
````
|
||||
|
@ -2,6 +2,10 @@ Link to [[pure-markdown-examples]] and the same [[Pure-Markdown-Examples]].
|
||||
|
||||
Link to [[pure-markdown-examples|pure markdown examples]].
|
||||
|
||||
Link to [[pure-markdown-examples#Heading 1]].
|
||||
|
||||
Link to [[pure-markdown-examples#Heading 1|pure markdown examples]].
|
||||
|
||||
Link within backticks: `[[pure-markdown-examples]]`
|
||||
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user