mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-07-02 04:47:01 +08:00
Overhaul Link parsing
We were mixing up header links with wiki links and the alt text. It was a bit messy. We currently do not support linking to a particular part of a note. Nor do we support wiki links as link references. Fixes APP-A0
This commit is contained in:
@ -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<Link> links = [];
|
||||
|
||||
LinkExtractor(this.filePath);
|
||||
|
||||
@override
|
||||
bool visitElementBefore(md.Element element) {
|
||||
return true;
|
||||
@ -42,25 +63,44 @@ 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<Link> visit(List<md.Node> nodes) {
|
||||
@ -69,6 +109,23 @@ class LinkExtractor implements md.NodeVisitor {
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
String _getText(List<md.Node> 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]]
|
||||
|
@ -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<List<Link>> parseLinks(String body, String parentFolderPath) async {
|
||||
Future<List<Link>> 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<Link>);
|
||||
@ -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<Link> _parseLinks(String body, String parentFolderPath) {
|
||||
@visibleForTesting
|
||||
List<Link> 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<Link> _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 = <Link>[];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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: () => "",
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
Reference in New Issue
Block a user