diff --git a/lib/core/link.dart b/lib/core/link.dart
index b039782d..994c0b3b 100644
--- a/lib/core/link.dart
+++ b/lib/core/link.dart
@@ -2,10 +2,22 @@ import 'package:markdown/markdown.dart' as md;
import 'package:meta/meta.dart';
class Link {
- String term;
- String filePath;
+ String publicTerm = "";
+ String filePath = "";
+ String headingID = "";
+ String alt = "";
- Link({@required this.term, @required this.filePath});
+ String wikiTerm = "";
+
+ Link({
+ @required this.publicTerm,
+ @required this.filePath,
+ this.headingID = "",
+ this.alt = "",
+ });
+ Link.wiki(this.wikiTerm);
+
+ bool get isWikiLink => wikiTerm.isNotEmpty;
@override
int get hashCode => filePath.hashCode;
@@ -15,17 +27,26 @@ class Link {
identical(this, other) ||
other is Link &&
runtimeType == other.runtimeType &&
- filePath == other.filePath;
+ filePath == other.filePath &&
+ publicTerm == other.publicTerm &&
+ wikiTerm == other.wikiTerm &&
+ headingID == other.headingID &&
+ alt == other.alt;
@override
String toString() {
- return 'Link{term: $term, filePath: $filePath}';
+ return wikiTerm.isNotEmpty
+ ? 'WikiLink($wikiTerm)'
+ : 'Link{publicTerm: $publicTerm, filePath: $filePath, headingID: $headingID}';
}
}
class LinkExtractor implements md.NodeVisitor {
+ final String filePath;
List links = [];
+ LinkExtractor(this.filePath);
+
@override
bool visitElementBefore(md.Element element) {
return true;
@@ -42,33 +63,69 @@ class LinkExtractor implements md.NodeVisitor {
var type = el.attributes['type'] ?? "";
if (type == "wiki") {
var term = el.attributes['term'];
- var link = Link(term: term, filePath: null);
+ var link = Link.wiki(term);
+
+ assert(link.filePath.isEmpty);
+ assert(link.publicTerm.isEmpty);
+ assert(link.alt.isEmpty);
+ assert(link.headingID.isEmpty);
+
links.add(link);
return;
}
- var title = el.attributes['title'] ?? "";
- if (title.isEmpty) {
- for (var child in el.children) {
- if (child is md.Text) {
- title += child.text;
- }
- }
- }
+ var alt = el.attributes['title'] ?? "";
+ var title = _getText(el.children);
var url = el.attributes['href'];
- var link = Link(term: title, filePath: url);
+ if (isExternalLink(url)) {
+ return;
+ }
+
+ if (url.startsWith('#')) {
+ var link = Link(
+ publicTerm: title,
+ filePath: filePath,
+ alt: alt,
+ headingID: url,
+ );
+ links.add(link);
+ return;
+ }
+
+ var link = Link(publicTerm: title, filePath: url, alt: alt);
links.add(link);
return;
}
}
+ static bool isExternalLink(String url) {
+ return url.startsWith(RegExp(r'[A-Za-z]{2,5}:\/\/'));
+ }
+
List visit(List nodes) {
for (final node in nodes) {
node.accept(this);
}
return links;
}
+
+ String _getText(List nodes) {
+ if (nodes == null) {
+ return "";
+ }
+
+ var text = "";
+ for (final node in nodes) {
+ if (node is md.Text) {
+ text += node.text;
+ } else if (node is md.Element) {
+ text += _getText(node.children);
+ }
+ }
+
+ return text;
+ }
}
/// Parse [[term]]
diff --git a/lib/core/links_loader.dart b/lib/core/links_loader.dart
index 1a99da8c..269a0283 100644
--- a/lib/core/links_loader.dart
+++ b/lib/core/links_loader.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'dart:isolate';
+import 'package:flutter/material.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path/path.dart' as p;
import 'package:synchronized/synchronized.dart';
@@ -31,11 +32,11 @@ class LinksLoader {
});
}
- Future> parseLinks(String body, String parentFolderPath) async {
+ Future> parseLinks({String body, String filePath}) async {
await _initIsolate();
var rec = ReceivePort();
- _sendPort.send(_LoadingMessage(body, parentFolderPath, rec.sendPort));
+ _sendPort.send(_LoadingMessage(body, filePath, rec.sendPort));
var data = await rec.first;
assert(data is List);
@@ -46,10 +47,10 @@ class LinksLoader {
class _LoadingMessage {
String body;
- String parentFolderPath;
+ String filePath;
SendPort sendPort;
- _LoadingMessage(this.body, this.parentFolderPath, this.sendPort);
+ _LoadingMessage(this.body, this.filePath, this.sendPort);
}
void _isolateMain(SendPort toMainSender) {
@@ -60,12 +61,15 @@ void _isolateMain(SendPort toMainSender) {
assert(data is _LoadingMessage);
var msg = data as _LoadingMessage;
- var links = _parseLinks(msg.body, msg.parentFolderPath);
+ var links = parseLinks(msg.body, msg.filePath);
msg.sendPort.send(links);
});
}
-List _parseLinks(String body, String parentFolderPath) {
+@visibleForTesting
+List parseLinks(String body, String filePath) {
+ var parentFolderPath = p.dirname(filePath);
+
final doc = md.Document(
encodeHtml: false,
extensionSet: md.ExtensionSet.gitHubFlavored,
@@ -74,29 +78,34 @@ List _parseLinks(String body, String parentFolderPath) {
var lines = body.replaceAll('\r\n', '\n').split('\n');
var nodes = doc.parseLines(lines);
- var possibleLinks = LinkExtractor().visit(nodes);
+ var possibleLinks = LinkExtractor(filePath).visit(nodes);
var links = [];
for (var l in possibleLinks) {
- var path = l.filePath;
- if (path == null) {
+ if (l.isWikiLink) {
links.add(l);
continue;
}
- var isLocal = !path.contains('://');
- if (isLocal) {
- l.filePath = p.join(parentFolderPath, p.normalize(l.filePath));
- links.add(l);
- }
+ l.filePath = p.join(parentFolderPath, p.normalize(l.filePath));
+ links.add(l);
}
doc.linkReferences.forEach((key, value) {
var path = value.destination;
- var isLocal = !path.contains('://');
- if (isLocal) {
- links.add(Link(term: key, filePath: path));
+ if (LinkExtractor.isExternalLink(path)) {
+ return;
}
+
+ var l = Link(publicTerm: value.label, filePath: "", alt: value.title);
+
+ if (path.startsWith('#') || path.startsWith('//')) {
+ l.headingID = path;
+ l.filePath = filePath;
+ } else {
+ l.filePath = p.join(parentFolderPath, p.normalize(path));
+ }
+ links.add(l);
});
return links;
diff --git a/lib/core/note.dart b/lib/core/note.dart
index b8fe8ee2..879d269f 100644
--- a/lib/core/note.dart
+++ b/lib/core/note.dart
@@ -512,7 +512,7 @@ class Note with NotesNotifier {
return _links;
}
- _links = await _linksLoader.parseLinks(_body, parent.folderPath);
+ _links = await _linksLoader.parseLinks(body: _body, filePath: _filePath);
return _links;
}
diff --git a/lib/utils/link_resolver.dart b/lib/utils/link_resolver.dart
index 7fcee2b0..a1332815 100644
--- a/lib/utils/link_resolver.dart
+++ b/lib/utils/link_resolver.dart
@@ -10,11 +10,12 @@ class LinkResolver {
LinkResolver(this.inputNote);
Note resolveLink(Link l) {
- if (l.filePath == null) {
- return resolveWikiLink(l.term);
+ if (l.isWikiLink) {
+ return resolveWikiLink(l.publicTerm);
}
var rootFolder = inputNote.parent.rootFolder;
+ assert(l.filePath.startsWith(rootFolder.folderPath));
var spec = l.filePath.substring(rootFolder.folderPath.length + 1);
return rootFolder.getNoteWithSpec(spec);
diff --git a/lib/widgets/notes_backlinks.dart b/lib/widgets/notes_backlinks.dart
index d9fabd03..e1d1f82b 100644
--- a/lib/widgets/notes_backlinks.dart
+++ b/lib/widgets/notes_backlinks.dart
@@ -173,7 +173,11 @@ class NoteSnippet extends StatelessWidget {
var body = note.body.split('\n');
var paragraph = body.firstWhere(
- (line) => line.contains('[${link.term}]'),
+ (String line) {
+ return link.isWikiLink
+ ? line.contains('[[${link.wikiTerm}}]]')
+ : line.contains('[${link.publicTerm}]');
+ },
orElse: () => "",
);
diff --git a/test/link_resolver_test.dart b/test/link_resolver_test.dart
index 8340d024..7021dba9 100644
--- a/test/link_resolver_test.dart
+++ b/test/link_resolver_test.dart
@@ -158,7 +158,7 @@ void main() {
var note = rootFolder.notes[0];
var linkResolver = LinkResolver(note);
- var link = Link(filePath: note.filePath, term: 'foo');
+ var link = Link(filePath: note.filePath, publicTerm: 'foo');
var resolvedNote = linkResolver.resolveLink(link);
expect(resolvedNote.filePath, note.filePath);
diff --git a/test/links_loader_test.dart b/test/links_loader_test.dart
index b4e32fb1..55bbab06 100644
--- a/test/links_loader_test.dart
+++ b/test/links_loader_test.dart
@@ -8,7 +8,7 @@ void main() {
[GitJournal](./gitjournal.md)
[GitJournal](gitjournal.md)
-[GitJournal](gitjournal)
+[GitJournal](gitjournal "alt-text")
[Google](https://google.com)
@@ -19,21 +19,81 @@ void main() {
test('Should load links', () async {
var loader = LinksLoader();
- var links = await loader.parseLinks(contents, "/tmp/foo");
+ var links = await loader.parseLinks(
+ body: contents,
+ filePath: "/tmp/foo/file.md",
+ );
- expect(links[0].filePath, null);
- expect(links[0].term, "GitJournal");
+ expect(links[0].filePath.isEmpty, true);
+ expect(links[0].headingID.isEmpty, true);
+ expect(links[0].alt.isEmpty, true);
+ expect(links[0].publicTerm.isEmpty, true);
+ expect(links[0].wikiTerm, "GitJournal");
+ expect(links[0].isWikiLink, true);
expect(links[1].filePath, "/tmp/foo/gitjournal.md");
- expect(links[1].term, "GitJournal");
+ expect(links[1].publicTerm, "GitJournal");
+ expect(links[1].alt.isEmpty, true);
+ expect(links[1].wikiTerm.isEmpty, true);
expect(links[2].filePath, "/tmp/foo/gitjournal.md");
- expect(links[2].term, "GitJournal");
+ expect(links[2].publicTerm, "GitJournal");
+ expect(links[2].alt.isEmpty, true);
+ expect(links[2].wikiTerm.isEmpty, true);
expect(links[3].filePath, "/tmp/foo/gitjournal");
- expect(links[3].term, "GitJournal");
+ expect(links[3].publicTerm, "GitJournal");
+ expect(links[3].alt, "alt-text");
+ expect(links[3].wikiTerm.isEmpty, true);
expect(links.length, 4);
});
+
+ test('Foam Documentation', () async {
+ var contents = """
+[](#contributors-)
+
+3. Use Foam's shortcuts and autocompletions to link your thoughts together with `[[wiki-links]]`, and navigate between them to explore your knowledge graph.
+4. the [[Graph Visualisation](https://foambubble.github.io/foam/graph-visualisation)], of [[Backlinking](https://foambubble.github.io/foam/backlinking)].
+
+
+
+Foam is licensed under the [MIT license](license).
+
+[//begin]: # "Autogenerated link references for markdown compatibility"
+[wiki-links]: wiki-links "Wiki Links"
+[//end]: # "Autogenerated link references"
+""";
+
+ var links = parseLinks(contents, "/tmp/foo.md");
+ expect(links.length, 5);
+
+ expect(links[0].filePath, "/tmp/foo.md");
+ expect(links[0].alt.isEmpty, true);
+ expect(links[0].headingID, "#contributors-");
+ expect(links[0].publicTerm.isEmpty, true);
+
+ expect(links[1].filePath, "/tmp/license");
+ expect(links[1].alt.isEmpty, true);
+ expect(links[1].headingID.isEmpty, true);
+ expect(links[1].publicTerm, "MIT license");
+
+ expect(links[2].filePath, "/tmp/foo.md");
+ expect(links[2].publicTerm, '//begin');
+ expect(links[2].headingID, "#");
+ expect(links[2].alt,
+ "Autogenerated link references for markdown compatibility");
+
+ // FIXME: link-references for wiki Links
+ // expect(links[3].filePath.isEmpty, true);
+ // expect(links[3].isWikiLink, true);
+ expect(links[3].headingID.isEmpty, true);
+ expect(links[3].alt, "Wiki Links");
+
+ expect(links[4].filePath, "/tmp/foo.md");
+ expect(links[4].publicTerm, '//end');
+ expect(links[4].headingID, "#");
+ expect(links[4].alt, "Autogenerated link references");
+ });
});
}
diff --git a/test/note_test.dart b/test/note_test.dart
index 16e71c1d..292fefc9 100644
--- a/test/note_test.dart
+++ b/test/note_test.dart
@@ -133,10 +133,10 @@ bar: Foo
var links = await note.fetchLinks();
expect(links[0].filePath, p.join(tempDir.path, "foo.md"));
- expect(links[0].term, "Hi");
+ expect(links[0].publicTerm, "Hi");
expect(links[1].filePath, p.join(tempDir.path, "food.md"));
- expect(links[1].term, "Hi2");
+ expect(links[1].publicTerm, "Hi2");
expect(links.length, 2);
});
@@ -152,11 +152,11 @@ bar: Foo
await note.load();
var links = await note.fetchLinks();
- expect(links[0].filePath, null);
- expect(links[0].term, "GitJournal");
+ expect(links[0].isWikiLink, true);
+ expect(links[0].wikiTerm, "GitJournal");
- expect(links[1].filePath, null);
- expect(links[1].term, "Wild Fire");
+ expect(links[1].isWikiLink, true);
+ expect(links[1].wikiTerm, "Wild Fire");
expect(links.length, 2);
});