mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-07-03 05:59:37 +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';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
class Link {
|
class Link {
|
||||||
String term;
|
String publicTerm = "";
|
||||||
String filePath;
|
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
|
@override
|
||||||
int get hashCode => filePath.hashCode;
|
int get hashCode => filePath.hashCode;
|
||||||
@ -15,17 +27,26 @@ class Link {
|
|||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is Link &&
|
other is Link &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
filePath == other.filePath;
|
filePath == other.filePath &&
|
||||||
|
publicTerm == other.publicTerm &&
|
||||||
|
wikiTerm == other.wikiTerm &&
|
||||||
|
headingID == other.headingID &&
|
||||||
|
alt == other.alt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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 {
|
class LinkExtractor implements md.NodeVisitor {
|
||||||
|
final String filePath;
|
||||||
List<Link> links = [];
|
List<Link> links = [];
|
||||||
|
|
||||||
|
LinkExtractor(this.filePath);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool visitElementBefore(md.Element element) {
|
bool visitElementBefore(md.Element element) {
|
||||||
return true;
|
return true;
|
||||||
@ -42,25 +63,44 @@ class LinkExtractor implements md.NodeVisitor {
|
|||||||
var type = el.attributes['type'] ?? "";
|
var type = el.attributes['type'] ?? "";
|
||||||
if (type == "wiki") {
|
if (type == "wiki") {
|
||||||
var term = el.attributes['term'];
|
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);
|
links.add(link);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = el.attributes['title'] ?? "";
|
var alt = el.attributes['title'] ?? "";
|
||||||
if (title.isEmpty) {
|
var title = _getText(el.children);
|
||||||
for (var child in el.children) {
|
|
||||||
if (child is md.Text) {
|
|
||||||
title += child.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = el.attributes['href'];
|
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);
|
links.add(link);
|
||||||
return;
|
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) {
|
List<Link> visit(List<md.Node> nodes) {
|
||||||
@ -69,6 +109,23 @@ class LinkExtractor implements md.NodeVisitor {
|
|||||||
}
|
}
|
||||||
return links;
|
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]]
|
/// Parse [[term]]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
import 'package:markdown/markdown.dart' as md;
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:synchronized/synchronized.dart';
|
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();
|
await _initIsolate();
|
||||||
|
|
||||||
var rec = ReceivePort();
|
var rec = ReceivePort();
|
||||||
_sendPort.send(_LoadingMessage(body, parentFolderPath, rec.sendPort));
|
_sendPort.send(_LoadingMessage(body, filePath, rec.sendPort));
|
||||||
|
|
||||||
var data = await rec.first;
|
var data = await rec.first;
|
||||||
assert(data is List<Link>);
|
assert(data is List<Link>);
|
||||||
@ -46,10 +47,10 @@ class LinksLoader {
|
|||||||
|
|
||||||
class _LoadingMessage {
|
class _LoadingMessage {
|
||||||
String body;
|
String body;
|
||||||
String parentFolderPath;
|
String filePath;
|
||||||
SendPort sendPort;
|
SendPort sendPort;
|
||||||
|
|
||||||
_LoadingMessage(this.body, this.parentFolderPath, this.sendPort);
|
_LoadingMessage(this.body, this.filePath, this.sendPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _isolateMain(SendPort toMainSender) {
|
void _isolateMain(SendPort toMainSender) {
|
||||||
@ -60,12 +61,15 @@ void _isolateMain(SendPort toMainSender) {
|
|||||||
assert(data is _LoadingMessage);
|
assert(data is _LoadingMessage);
|
||||||
var msg = data as _LoadingMessage;
|
var msg = data as _LoadingMessage;
|
||||||
|
|
||||||
var links = _parseLinks(msg.body, msg.parentFolderPath);
|
var links = parseLinks(msg.body, msg.filePath);
|
||||||
msg.sendPort.send(links);
|
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(
|
final doc = md.Document(
|
||||||
encodeHtml: false,
|
encodeHtml: false,
|
||||||
extensionSet: md.ExtensionSet.gitHubFlavored,
|
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 lines = body.replaceAll('\r\n', '\n').split('\n');
|
||||||
var nodes = doc.parseLines(lines);
|
var nodes = doc.parseLines(lines);
|
||||||
var possibleLinks = LinkExtractor().visit(nodes);
|
var possibleLinks = LinkExtractor(filePath).visit(nodes);
|
||||||
|
|
||||||
var links = <Link>[];
|
var links = <Link>[];
|
||||||
for (var l in possibleLinks) {
|
for (var l in possibleLinks) {
|
||||||
var path = l.filePath;
|
if (l.isWikiLink) {
|
||||||
if (path == null) {
|
|
||||||
links.add(l);
|
links.add(l);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isLocal = !path.contains('://');
|
|
||||||
if (isLocal) {
|
|
||||||
l.filePath = p.join(parentFolderPath, p.normalize(l.filePath));
|
l.filePath = p.join(parentFolderPath, p.normalize(l.filePath));
|
||||||
links.add(l);
|
links.add(l);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
doc.linkReferences.forEach((key, value) {
|
doc.linkReferences.forEach((key, value) {
|
||||||
var path = value.destination;
|
var path = value.destination;
|
||||||
var isLocal = !path.contains('://');
|
if (LinkExtractor.isExternalLink(path)) {
|
||||||
if (isLocal) {
|
return;
|
||||||
links.add(Link(term: key, filePath: path));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return links;
|
||||||
|
@ -512,7 +512,7 @@ class Note with NotesNotifier {
|
|||||||
return _links;
|
return _links;
|
||||||
}
|
}
|
||||||
|
|
||||||
_links = await _linksLoader.parseLinks(_body, parent.folderPath);
|
_links = await _linksLoader.parseLinks(body: _body, filePath: _filePath);
|
||||||
return _links;
|
return _links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,12 @@ class LinkResolver {
|
|||||||
LinkResolver(this.inputNote);
|
LinkResolver(this.inputNote);
|
||||||
|
|
||||||
Note resolveLink(Link l) {
|
Note resolveLink(Link l) {
|
||||||
if (l.filePath == null) {
|
if (l.isWikiLink) {
|
||||||
return resolveWikiLink(l.term);
|
return resolveWikiLink(l.publicTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootFolder = inputNote.parent.rootFolder;
|
var rootFolder = inputNote.parent.rootFolder;
|
||||||
|
assert(l.filePath.startsWith(rootFolder.folderPath));
|
||||||
var spec = l.filePath.substring(rootFolder.folderPath.length + 1);
|
var spec = l.filePath.substring(rootFolder.folderPath.length + 1);
|
||||||
|
|
||||||
return rootFolder.getNoteWithSpec(spec);
|
return rootFolder.getNoteWithSpec(spec);
|
||||||
|
@ -173,7 +173,11 @@ class NoteSnippet extends StatelessWidget {
|
|||||||
|
|
||||||
var body = note.body.split('\n');
|
var body = note.body.split('\n');
|
||||||
var paragraph = body.firstWhere(
|
var paragraph = body.firstWhere(
|
||||||
(line) => line.contains('[${link.term}]'),
|
(String line) {
|
||||||
|
return link.isWikiLink
|
||||||
|
? line.contains('[[${link.wikiTerm}}]]')
|
||||||
|
: line.contains('[${link.publicTerm}]');
|
||||||
|
},
|
||||||
orElse: () => "",
|
orElse: () => "",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ void main() {
|
|||||||
var note = rootFolder.notes[0];
|
var note = rootFolder.notes[0];
|
||||||
var linkResolver = LinkResolver(note);
|
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);
|
var resolvedNote = linkResolver.resolveLink(link);
|
||||||
expect(resolvedNote.filePath, note.filePath);
|
expect(resolvedNote.filePath, note.filePath);
|
||||||
|
@ -8,7 +8,7 @@ void main() {
|
|||||||
|
|
||||||
[GitJournal](./gitjournal.md)
|
[GitJournal](./gitjournal.md)
|
||||||
[GitJournal](gitjournal.md)
|
[GitJournal](gitjournal.md)
|
||||||
[GitJournal](gitjournal)
|
[GitJournal](gitjournal "alt-text")
|
||||||
|
|
||||||
[Google](https://google.com)
|
[Google](https://google.com)
|
||||||
|
|
||||||
@ -19,21 +19,81 @@ void main() {
|
|||||||
|
|
||||||
test('Should load links', () async {
|
test('Should load links', () async {
|
||||||
var loader = LinksLoader();
|
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].filePath.isEmpty, true);
|
||||||
expect(links[0].term, "GitJournal");
|
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].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].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].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);
|
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();
|
var links = await note.fetchLinks();
|
||||||
expect(links[0].filePath, p.join(tempDir.path, "foo.md"));
|
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].filePath, p.join(tempDir.path, "food.md"));
|
||||||
expect(links[1].term, "Hi2");
|
expect(links[1].publicTerm, "Hi2");
|
||||||
|
|
||||||
expect(links.length, 2);
|
expect(links.length, 2);
|
||||||
});
|
});
|
||||||
@ -152,11 +152,11 @@ bar: Foo
|
|||||||
await note.load();
|
await note.load();
|
||||||
|
|
||||||
var links = await note.fetchLinks();
|
var links = await note.fetchLinks();
|
||||||
expect(links[0].filePath, null);
|
expect(links[0].isWikiLink, true);
|
||||||
expect(links[0].term, "GitJournal");
|
expect(links[0].wikiTerm, "GitJournal");
|
||||||
|
|
||||||
expect(links[1].filePath, null);
|
expect(links[1].isWikiLink, true);
|
||||||
expect(links[1].term, "Wild Fire");
|
expect(links[1].wikiTerm, "Wild Fire");
|
||||||
|
|
||||||
expect(links.length, 2);
|
expect(links.length, 2);
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user