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",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deunicode"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "difference"
|
name = "difference"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -305,6 +311,7 @@ dependencies = [
|
|||||||
"pulldown-cmark-to-cmark",
|
"pulldown-cmark-to-cmark",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
|
"slug",
|
||||||
"snafu",
|
"snafu",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@ -508,6 +515,15 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slug"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
|
||||||
|
dependencies = [
|
||||||
|
"deunicode",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snafu"
|
name = "snafu"
|
||||||
version = "0.6.10"
|
version = "0.6.10"
|
||||||
|
@ -35,6 +35,7 @@ pulldown-cmark = "0.8.0"
|
|||||||
pulldown-cmark-to-cmark = "6.0.0"
|
pulldown-cmark-to-cmark = "6.0.0"
|
||||||
rayon = "1.5.0"
|
rayon = "1.5.0"
|
||||||
regex = "1.4.2"
|
regex = "1.4.2"
|
||||||
|
slug = "0.1.4"
|
||||||
snafu = "0.6.10"
|
snafu = "0.6.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[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 pulldown_cmark_to_cmark::cmark_with_options;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use slug::slugify;
|
||||||
use snafu::{ResultExt, Snafu};
|
use snafu::{ResultExt, Snafu};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::fmt;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
@ -196,6 +198,24 @@ impl<'a> ObsidianNoteReference<'a> {
|
|||||||
section,
|
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> {
|
impl<'a> Exporter<'a> {
|
||||||
@ -384,9 +404,11 @@ impl<'a> Exporter<'a> {
|
|||||||
if let Event::Text(CowStr::Borrowed(text)) = buffer[2] {
|
if let Event::Text(CowStr::Borrowed(text)) = buffer[2] {
|
||||||
match buffer[0] {
|
match buffer[0] {
|
||||||
Event::Text(CowStr::Borrowed("[")) => {
|
Event::Text(CowStr::Borrowed("[")) => {
|
||||||
let mut link_events =
|
let mut elements = self.make_link_to_file(
|
||||||
self.obsidian_note_link_to_markdown(&text, context);
|
ObsidianNoteReference::from_str(&text),
|
||||||
tree.append(&mut link_events);
|
context,
|
||||||
|
);
|
||||||
|
tree.append(&mut elements);
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -445,7 +467,7 @@ impl<'a> Exporter<'a> {
|
|||||||
tree
|
tree
|
||||||
}
|
}
|
||||||
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("webp") => {
|
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()
|
.into_iter()
|
||||||
.map(|event| match event {
|
.map(|event| match event {
|
||||||
// make_link_to_file returns a link to a file. With this we turn the link
|
// 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()
|
.collect()
|
||||||
}
|
}
|
||||||
_ => self.make_link_to_file(¬e_ref.file, ¬e_ref.file, &context),
|
_ => self.make_link_to_file(note_ref, &context),
|
||||||
};
|
};
|
||||||
Ok(tree)
|
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>(
|
fn make_link_to_file<'b>(
|
||||||
&self,
|
&self,
|
||||||
file: &'b str,
|
reference: ObsidianNoteReference<'b>,
|
||||||
label: &'b str,
|
|
||||||
context: &Context,
|
context: &Context,
|
||||||
) -> MarkdownTree<'b> {
|
) -> 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() {
|
if target_file.is_none() {
|
||||||
// TODO: Extract into configurable function.
|
// TODO: Extract into configurable function.
|
||||||
println!(
|
println!(
|
||||||
"Warning: Unable to find referenced note\n\tReference: '{}'\n\tSource: '{}'\n",
|
"Warning: Unable to find referenced note\n\tReference: '{}'\n\tSource: '{}'\n",
|
||||||
file,
|
reference.file,
|
||||||
context.current_file().display(),
|
context.current_file().display(),
|
||||||
);
|
);
|
||||||
return vec![
|
return vec![
|
||||||
Event::Start(Tag::Emphasis),
|
Event::Start(Tag::Emphasis),
|
||||||
Event::Text(CowStr::from(String::from(label))),
|
Event::Text(CowStr::from(reference.display())),
|
||||||
Event::End(Tag::Emphasis),
|
Event::End(Tag::Emphasis),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -513,19 +529,25 @@ impl<'a> Exporter<'a> {
|
|||||||
.expect("obsidian content files should always have a parent"),
|
.expect("obsidian content files should always have a parent"),
|
||||||
)
|
)
|
||||||
.expect("should be able to build relative path when target file is found in vault");
|
.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,
|
pulldown_cmark::LinkType::Inline,
|
||||||
CowStr::from(encoded_link.to_string()),
|
CowStr::from(link.to_string()),
|
||||||
CowStr::from(""),
|
CowStr::from(""),
|
||||||
);
|
);
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
Event::Start(link.clone()),
|
Event::Start(link_tag.clone()),
|
||||||
Event::Text(CowStr::from(label)),
|
Event::Text(CowStr::from(reference.display())),
|
||||||
Event::End(link.clone()),
|
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](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]]`
|
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|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]]`
|
Link within backticks: `[[pure-markdown-examples]]`
|
||||||
|
|
||||||
```
|
```
|
||||||
|
Loading…
Reference in New Issue
Block a user