From cb83f260f7d76f3360fdf9cd81f2450cdb84afa8 Mon Sep 17 00:00:00 2001 From: Matthias Bilger Date: Tue, 29 Aug 2023 21:07:39 +0200 Subject: [PATCH 1/2] Add annotated links, and corresponding filter --- core/modules/filters/annotatedlinks.js | 45 ++++++ .../wikiparser/rules/prettyannotatedlink.js | 147 ++++++++++++++++++ core/modules/wiki.js | 108 +++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 core/modules/filters/annotatedlinks.js create mode 100644 core/modules/parsers/wikiparser/rules/prettyannotatedlink.js diff --git a/core/modules/filters/annotatedlinks.js b/core/modules/filters/annotatedlinks.js new file mode 100644 index 000000000..2ef6627bd --- /dev/null +++ b/core/modules/filters/annotatedlinks.js @@ -0,0 +1,45 @@ +/*\ +title: $:/core/modules/filters/annotatedlinks.js +type: application/javascript +module-type: filteroperator + +Filter operator for returning all the links from a tiddler with a given annotation + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +function parseAnnotations(annotationstring){ + var annotations = {} + if (annotationstring.length > 0){ + annotationstring.split(',').forEach(function(part){ + if (part.includes('=')){ + let subparts = part.split('=', 2); + let key = subparts[0], + value = subparts[1]; + annotations[key] = value; + }else{ + annotations[part] = "*"; + } + }); + } + return annotations; +} + +/* +Export our filter function +*/ +exports.annotatedlinks = function(source,operator,options) { + var results = new $tw.utils.LinkedList(); + source(function(tiddler,title) { + var annotations = parseAnnotations(operator.operands[0]); + results.pushTop(options.wiki.getTiddlerAnnotatedLinks(title, annotations)); + }); + return results.makeTiddlerIterator(options.wiki); +}; + + +})(); diff --git a/core/modules/parsers/wikiparser/rules/prettyannotatedlink.js b/core/modules/parsers/wikiparser/rules/prettyannotatedlink.js new file mode 100644 index 000000000..e7c207ccb --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/prettyannotatedlink.js @@ -0,0 +1,147 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/prettyannotatedlink.js +type: application/javascript +module-type: wikirule + +Wiki text inline rule for pretty links with annotations. For example: + +``` +[link attribute="value" [Link description|TiddlerTitle]] +[link attribute="value" [TiddlerTitle]] +``` + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.name = "prettyannotatedlink"; +exports.types = {inline: true}; + +exports.init = function(parser) { + this.parser = parser; +}; + +exports.findNextMatch = function(startPos) { + // Find the next tag + this.nextAnnotatedLink = this.findNextAnnotatedLink(this.parser.source,startPos); + return this.nextAnnotatedLink ? this.nextAnnotatedLink.start : undefined; +}; + +exports.parse = function() { + // Move past the match + this.parser.pos = this.nextAnnotatedLink.end; + if($tw.utils.isLinkExternal(this.nextAnnotatedLink.attributes.to)) { + return [{ + type: "element", + tag: "a", + attributes: { + href: {type: "string", value: link}, + "class": {type: "string", value: "tc-tiddlylink-external"}, + target: {type: "string", value: "_blank"}, + rel: {type: "string", value: "noopener noreferrer"}, + ...this.nextAnnotatedLink.attributes + }, + children: [{ + type: "text", text: this.nextAnnotatedLink.text + }] + }]; + } else { + return [{ + type: "link", + attributes: this.nextAnnotatedLink.attributes, + children: [{ + type: "text", text: this.nextAnnotatedLink.text + }] + }]; + } +}; + +/* +Find the next link from the current position +*/ +exports.findNextAnnotatedLink = function(source,pos) { + // A regexp for finding candidate HTML tags + var reLookahead = /(\[link)/g; + // Find the next candidate + reLookahead.lastIndex = pos; + var match = reLookahead.exec(source); + while(match) { + // Try to parse the candidate as a tag + var tag = this.parseAnnotatedLink(source,match.index); + // Return success + if(tag) { + return tag; + } + // Look for the next match + reLookahead.lastIndex = match.index + 1; + match = reLookahead.exec(source); + } + // Failed + return null; +}; + +/* +Look for an link at the specified position. Returns null if not found, otherwise returns {type: "link", attributes: [], isSelfClosing:, start:, end:,} +*/ +exports.parseAnnotatedLink = function(source,pos) { + var token, + node = { + type: "link", + start: pos, + attributes: {}, + text: "" + }; + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Look for the `[img` + token = $tw.utils.parseTokenString(source,pos,"[link"); + if(!token) { + return null; + } + pos = token.end; + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Process attributes + if(source.charAt(pos) !== "[") { + var attribute = $tw.utils.parseAttribute(source,pos); + while(attribute) { + node.attributes[attribute.name] = attribute; + pos = attribute.end; + pos = $tw.utils.skipWhiteSpace(source,pos); + if(source.charAt(pos) !== "[") { + // Get the next attribute + attribute = $tw.utils.parseAttribute(source,pos); + } else { + attribute = null; + } + } + } + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Look for the `[` after the attributes + token = $tw.utils.parseTokenString(source,pos,"["); + if(!token) { + return null; + } + pos = token.end; + // Skip whitespace + pos = $tw.utils.skipWhiteSpace(source,pos); + // Get the source up to the terminating `]]` + token = $tw.utils.parseTokenRegExp(source,pos,/(.*?)(?:\|(.*?))?\]\]/g); + if(!token) { + return null; + } + pos = token.end; + var text = token.match[1], + link = token.match[2] || text; + node.attributes.to = {type: "string", value: link}; + node.text = text; + // Update the end position + node.end = pos; + return node; +}; + +})(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 3eae3902d..4c8e70772 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -549,6 +549,114 @@ exports.getTiddlerBacklinks = function(targetTitle) { return backlinks; }; +/* +Return an array of tiddler titles that are directly linked within the given parse tree +annotations: This could either be: + * a string: to look for the availability of a single annotation + * an array: for any of the listed annotations + * an object: defining a number of key value pairs, where either + * the value is function which must return true + * the value is '*' which allows any value + * the value given matches exactly the annotation + */ +exports.extractAnnotatedLinks = function(parseTreeRoot, annotations) { + var annotation_list = {}; + switch(typeof(annotations)){ + case "string": + if (annotations.length==0){ + return []; + } + let key = annotations; + annotation_list[key] = '*'; + break; + case "object": + let original_annotations = annotations; + if (Array.isArray(annotations)){ + annotations.forEach((key) => { + annotation_list[key] = '*'; + }); + }else{ + annotation_list = annotations; + } + break; + } + // Count up the links + var links = [], + checkParseTree = function(parseTree) { + for(var t=0; t Date: Wed, 30 Aug 2023 21:21:49 +0200 Subject: [PATCH 2/2] Added annotatedbacklinks filter --- core/modules/filters/annotatedbacklinks.js | 46 ++++++++++++++++++++++ core/modules/wiki.js | 43 +++++++------------- 2 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 core/modules/filters/annotatedbacklinks.js diff --git a/core/modules/filters/annotatedbacklinks.js b/core/modules/filters/annotatedbacklinks.js new file mode 100644 index 000000000..1d89270c9 --- /dev/null +++ b/core/modules/filters/annotatedbacklinks.js @@ -0,0 +1,46 @@ +/*\ +title: $:/core/modules/filters/annotatedbacklinks.js +type: application/javascript +module-type: filteroperator + +Filter operator for returning all the back links from a tiddler with a given annotation + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +function parseAnnotations(annotationstring){ + var annotations = {} + if (annotationstring.length > 0){ + annotationstring.split(',').forEach(function(part){ + if (part.includes('=')){ + let subparts = part.split('=', 2); + let key = subparts[0], + value = subparts[1]; + annotations[key] = value; + }else{ + annotations[part] = "*"; + } + }); + } + return annotations; +} + +/* +Export our filter function +*/ +exports.annotatedbacklinks = function(source,operator,options) { + var results = new $tw.utils.LinkedList(); + source(function(tiddler,title) { + var annotations = parseAnnotations(operator.operands[0]); + console.log(annotations); + results.pushTop(options.wiki.getTiddlerAnnotatedBacklinks(title, annotations)); + }); + return results.makeTiddlerIterator(options.wiki); +}; + + +})(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 4c8e70772..deb37597c 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -585,6 +585,7 @@ exports.extractAnnotatedLinks = function(parseTreeRoot, annotations) { checkParseTree = function(parseTree) { for(var t=0; t