Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: allow link to section mark #7744

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5b6f5b2
feat: parse and show ^id
linonetwo Sep 15, 2023
4c407c2
refactor: use blockid for shorter name
linonetwo Sep 15, 2023
18236b5
feat: allow add id for code block
linonetwo Sep 15, 2023
d5e9d2a
feat: allow wiki pretty link to have id
linonetwo Sep 15, 2023
b956e72
fix: properly match blockId and pass it to ast
linonetwo Sep 15, 2023
3d8ade3
feat: redirect tm-focus-selector event to check parent or sibling
linonetwo Sep 15, 2023
6b61243
fix: param maybe null
linonetwo Sep 15, 2023
d8bdd09
fix: ensure hightlight is visible
linonetwo Sep 15, 2023
0863916
fix: wait until animation finish and dom show
linonetwo Sep 15, 2023
e6445b7
docs: why add hook
linonetwo Sep 16, 2023
db83401
docs: about usage
linonetwo Sep 16, 2023
7200f73
refactor: use th-navigated to simplify the code
linonetwo Sep 16, 2023
c0b6b79
fix: element not exist
linonetwo Sep 16, 2023
3bcd822
fix: scroll too slow if tiddler already appear
linonetwo Sep 16, 2023
07130c2
fix: code style and types
linonetwo Sep 16, 2023
a5c2f85
feat: allow different tiddler have same block id in the text, and onl…
linonetwo Sep 17, 2023
cff0240
feat: allow using any char in id
linonetwo Sep 17, 2023
fef444c
fix: when id not exist, still navigate to the tiddler
linonetwo Sep 17, 2023
0d18b25
Update blockid.js
linonetwo Sep 18, 2023
dfa0600
docs: about why history change will reflect on storyview
linonetwo Sep 22, 2023
436343c
refactor: use history mechanism for block level navigation
linonetwo Sep 22, 2023
ebf84b5
docs: about HistoryMechanism in dev doc
linonetwo Sep 22, 2023
507d004
feat: adapt for other story views
linonetwo Sep 22, 2023
48d2eff
fix: no need for setTimeout
linonetwo Sep 22, 2023
13f6bd8
refactor: remove unusned hook
linonetwo Sep 22, 2023
13d0c5c
docs: about toAnchor added in 5.3.2
linonetwo Sep 22, 2023
6d16c98
Merge remote-tracking branch 'upstream/master' into feat/section-mark
linonetwo Jun 9, 2024
9c2c6d7
fix: link end position should add ^id 's length
linonetwo Jun 9, 2024
7e9cadf
Revert "fix: link end position should add ^id 's length"
linonetwo Jun 9, 2024
92ca17a
fix: correct anchor start end
linonetwo Jun 9, 2024
cc12af5
docs: fix BlockIdWidget
linonetwo Jun 9, 2024
cb2d4bb
Merge branch 'master' into feat/section-mark
linonetwo Sep 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions boot/boot.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions core/modules/parsers/wikiparser/rules/blockid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*\
title: $:/core/modules/parsers/wikiparser/rules/blockidentifier.js
type: application/javascript
module-type: wikirule

Use hash as a tag for paragraph, we call it block identifier.

1. Hash won't change, it can be written by hand or be generated, and it is a ` \^\S+$` string after line: `text ^cb9d485` or `text ^1`, so it can be human readable (while without space), here are the parse rule for this.
2. When creating widgets for rendering, omit this hash, so it's invisible in view mode. But this widget will create an anchor to jump to.

\*/
exports.name = "blockid";
exports.types = {inline: true};

/*
Instantiate parse rule
*/
exports.init = function(parser) {
this.parser = parser;
// Regexp to match the block identifier
// 1. located on the end of the line, with a space before it, means it's the id of the current block.
// 2. located at start of the line, no space, means it's the id of the previous block. Because some block can't have id suffix, otherwise id break the block mode parser like codeblock.
this.matchRegExp = /[ ]\^(\S+)$|^\^(\S+)$/mg;
};

/*
Parse the most recent match
*/
exports.parse = function() {
// Move past the match
this.parser.pos = this.matchRegExp.lastIndex;
// will be one of following case, another will be undefined
var blockId = this.match[1];
var blockBeforeId = this.match[2];
// Parse tree nodes to return
return [{
type: "blockid",
attributes: {
id: {type: "string", value: blockId || blockBeforeId},
// `yes` means the block is before this node, in parent node's children list.
// empty means the block is this node's direct parent node.
previousSibling: {type: "string", value: Boolean(blockBeforeId) ? "yes" : ""},
},
children: []
}];
};
14 changes: 10 additions & 4 deletions core/modules/parsers/wikiparser/rules/prettylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ exports.types = {inline: true};

exports.init = function(parser) {
this.parser = parser;
// Regexp to match
this.matchRegExp = /\[\[(.*?)(?:\|(.*?))?\]\]/mg;
// Regexp to match `[[Alias|Title^blockId]]`, the `Alias|` and `^blockId` are optional.
this.matchRegExp = /\[\[(.*?)(?:\|(.*?)?)?(?:\^([^|\s^]+)?)?\]\]/mg;
};

exports.parse = function() {
// Move past the match
this.parser.pos = this.matchRegExp.lastIndex;
// Process the link
var text = this.match[1],
link = this.match[2] || text;
link = this.match[2] || text,
blockId = this.match[3] || "";
if($tw.utils.isLinkExternal(link)) {
// add back the part after `^` to the ext link, if it happen to has one.
if(blockId) {
link = link + "^" + blockId;
}
return [{
type: "element",
tag: "a",
Expand All @@ -51,7 +56,8 @@ exports.parse = function() {
return [{
type: "link",
attributes: {
to: {type: "string", value: link}
to: {type: "string", value: link},
toBlockId: {type: "string", value: blockId},
},
children: [{
type: "text", text: text
Expand Down
133 changes: 133 additions & 0 deletions core/modules/widgets/blockid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*\
title: $:/core/modules/widgets/blockid.js
type: application/javascript
module-type: widget

An invisible element with block id metadata.
\*/
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var BlockIdWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
// only this widget knows target info (if the block is before this node or not), so we need to hook the focus event, and process it here, instead of in the root widget.
this.hookNavigationAddHistoryEvent = this.hookNavigationAddHistoryEvent.bind(this);
this.hookNavigatedEvent = this.hookNavigatedEvent.bind(this);
$tw.hooks.addHook("th-navigating-add-history",this.hookNavigationAddHistoryEvent);
$tw.hooks.addHook("th-navigated",this.hookNavigatedEvent);
};
BlockIdWidget.prototype = new Widget();

BlockIdWidget.prototype.removeChildDomNodes = function() {
$tw.hooks.removeHook("th-navigating-add-history",this.hookNavigationAddHistoryEvent);
$tw.hooks.removeHook("th-navigated",this.hookNavigatedEvent);
};

BlockIdWidget.prototype.render = function(parent,nextSibling) {
// Save the parent dom node
this.parentDomNode = parent;
// Compute our attributes
this.computeAttributes();
// Execute our logic
this.execute();
// Create an invisible DOM element with data that can be accessed from JS or CSS
this.idNode = this.document.createElement("span");
this.idNode.setAttribute("data-block-id",this.id);
this.idNode.setAttribute("data-block-title",this.tiddlerTitle);
if(this.before) {
this.idNode.setAttribute("data-before","true");
}
this.idNode.className = "tc-block-id";
parent.insertBefore(this.idNode,nextSibling);
this.domNodes.push(this.idNode);
};

BlockIdWidget.prototype._isNavigateToHere = function(event) {
if(!event || !event.toBlockId) return false;
if(event.toBlockId !== this.id) return false;
if(this.tiddlerTitle && event.navigateTo !== this.tiddlerTitle) return false;
return true;
}

BlockIdWidget.prototype.hookNavigatedEvent = function(event) {
if(!this._isNavigateToHere(event)) return event;
var baseElement = event.event && event.event.target ? event.event.target.ownerDocument : document;
var element = this._getTargetElement(baseElement);
if(element) {
// if tiddler is already in the story view, just move to it.
this._scrollToBlockAndHighlight(element);
} else {
var self = this;
// Here we still need to wait for extra time after `duration`, so tiddler dom is actually added to the story view.
var duration = $tw.utils.getAnimationDuration() + 50;
setTimeout(function() {
element = self._getTargetElement(baseElement);
self._scrollToBlockAndHighlight(element);
}, duration);
}
return false;
};

BlockIdWidget.prototype.hookNavigationAddHistoryEvent = function(event) {
// DEBUG: console this._isNavigateToHere(event)
console.log(`this._isNavigateToHere(event)`, this._isNavigateToHere(event));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the TW core we still use ES5 only. So JS template literals are not possible atm: `this._isNavigateToHere(event)`
So this code needs to be changed.

Copy link
Contributor Author

@linonetwo linonetwo Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is debug statement, forget to delete...

Copy link
Contributor Author

@linonetwo linonetwo Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created by this in vscode BTW

  "Print to console": {
    "prefix": "lo",
    "scope": "javascript,javascriptreact,typescript,typescriptreact",
    "body": ["// DEBUG: console $1", "console.log(`$1`, $1);"],
    "description": "Log formated JSON output and var name to console"
  },

if(!this._isNavigateToHere(event)) return event;
event.navigateSuppressNavigation = true;
return event;
};

BlockIdWidget.prototype._getTargetElement = function(baseElement) {
var selector = "span[data-block-id='"+this.id+"']";
if(this.tiddlerTitle) {
// allow different tiddler have same block id in the text, and only jump to the one with a same tiddler title.
selector += "[data-block-title='"+this.tiddlerTitle+"']";
}
// re-query the dom node, because `this.idNode.parentNode` might already be removed from document
var element = $tw.utils.querySelectorSafe(selector,baseElement);
if(!element || !element.parentNode) return;
// the actual block is always at the parent level
element = element.parentNode;
// need to check if the block is before this node
if(this.previousSibling && element.previousSibling) {
element = element.previousSibling;
}
return element;
};

BlockIdWidget.prototype._scrollToBlockAndHighlight = function(element) {
if(!element) return;
// toggle class to trigger highlight animation
$tw.utils.removeClass(element,"tc-focus-highlight");
// We enable the `navigateSuppressNavigation` in LinkWidget when sending `tm-navigate`, otherwise `tm-navigate` will force move to the title
element.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
element.focus({ focusVisible: true });
// Using setTimeout to ensure the removal takes effect before adding the class again.
setTimeout(function() {
$tw.utils.addClass(element,"tc-focus-highlight");
}, 50);
};

/*
Compute the internal state of the widget
*/
BlockIdWidget.prototype.execute = function() {
// Get the id from the parse tree node or manually assigned attributes
this.id = this.getAttribute("id");
this.tiddlerTitle = this.getVariable("currentTiddler");
this.previousSibling = this.getAttribute("previousSibling") === "yes";
// Make the child widgets
this.makeChildWidgets();
};

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
BlockIdWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(($tw.utils.count(changedAttributes) > 0)) {
this.refreshSelf();
return true;
} else {
return this.refreshChildren(changedTiddlers);
}
};

exports.blockid = BlockIdWidget;
2 changes: 2 additions & 0 deletions core/modules/widgets/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ LinkWidget.prototype.handleClickEvent = function(event) {
this.dispatchEvent({
type: "tm-navigate",
navigateTo: this.to,
toBlockId: this.toBlockId,
navigateFromTitle: this.getVariable("storyTiddler"),
navigateFromNode: this,
navigateFromClientRect: { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
Expand Down Expand Up @@ -180,6 +181,7 @@ Compute the internal state of the widget
LinkWidget.prototype.execute = function() {
// Pick up our attributes
this.to = this.getAttribute("to",this.getVariable("currentTiddler"));
this.toBlockId = this.getAttribute("toBlockId");
this.tooltip = this.getAttribute("tooltip");
this["aria-label"] = this.getAttribute("aria-label");
this.linkClasses = this.getAttribute("class");
Expand Down
2 changes: 2 additions & 0 deletions core/modules/widgets/navigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ NavigatorWidget.prototype.handleNavigateEvent = function(event) {
event = $tw.hooks.invokeHook("th-navigating",event);
if(event.navigateTo) {
this.addToStory(event.navigateTo,event.navigateFromTitle);
event = $tw.hooks.invokeHook("th-navigating-add-history",event);
if(!event.navigateSuppressNavigation) {
this.addToHistory(event.navigateTo,event.navigateFromClientRect);
}
}
$tw.hooks.invokeHook("th-navigated",event);
return false;
};

Expand Down
2 changes: 1 addition & 1 deletion core/ui/EditTemplate/title.tid
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ tags: $:/tags/EditTemplate
\whitespace trim
<$edit-text field="draft.title" class="tc-titlebar tc-edit-texteditor" focus={{{ [{$:/config/AutoFocus}match[title]then[true]] ~[[false]] }}} tabindex={{$:/config/EditTabIndex}} cancelPopups="yes"/>

<$vars pattern="""[\|\[\]{}]""" bad-chars="""`| [ ] { }`""">
<$vars pattern="""[\^\|\[\]{}]""" bad-chars="""`| ^ [ ] { }`""">

<$list filter="[all[current]regexp:draft.title<pattern>]" variable="listItem">

Expand Down
11 changes: 11 additions & 0 deletions editions/dev/tiddlers/new/Hook__th-navigated.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
tags: HookMechanism
title: Hook: th-navigated
type: text/vnd.tiddlywiki

This hook allows plugins to do things after navigation takes effect.

Hook function parameters are same as [[Hook: th-navigating]]:

Return value:

* possibly modified event object
34 changes: 34 additions & 0 deletions editions/tw5.com/tiddlers/widgets/BlockIdWidget.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
caption: block id
created: 20230916061829840
modified: 20230917121007649
tags: Widgets
title: BlockIdWidget
type: text/vnd.tiddlywiki

! Introduction

The block id widget make an anchor that can be focused and jump to.

! Content and Attributes

The content of the `<$blockid>` widget is ignored.

|!Attribute |!Description |
|id |The unique id for the block |
|previousSibling |`yes` means the block is before this node, in parent node's children list, else it means the block is this node's direct parent node. |

See [[Block Level Links in WikiText^🤗→AddingIDforblock]] for WikiText syntax of block ID.

! Example

<<wikitext-example-without-html """The block id widget is invisible, and is usually located at the end of the line. ID is here:<$blockid id="BlockLevelLinksID1"/>

[[Link to BlockLevelLinksID1|BlockIdWidget^BlockLevelLinksID1]]
""">>

<<wikitext-example """You can refer to the block that is a line before the block id widget. Make sure block id widget itself is in a block (paragraph).

ID is here:<$blockid id="BlockLevelLinksID2" previousSibling="yes"/>

[[Link to BlockLevelLinksID2|BlockIdWidget^BlockLevelLinksID2]]
""">>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
caption: Block Level Links
created: 20230916061138153
modified: 20230917122221226
tags: WikiText
title: Block Level Links in WikiText
type: text/vnd.tiddlywiki

! Adding ID for block ^🤗→AddingIDforblock

The basic syntax for block id is:

<<wikitext-example src:"There is a block id that is invisible, but you can find it using developer tool's inspect element feature. ^BlockLevelLinksID1">>

# Don't forget the space between the end of the line and the `^`.
# And there is no space between `^` and the id.
# ID can contain any char other than `^` and space ` `.

And this block id widget will be rendered as an invisible element:

```html
<span class="tc-block-id" data-block-id="BlockLevelLinksID1" data-block-title="Block Level Links in WikiText"></span>
```

!! Adding id to previous block

Some block, for example, code block, can't be suffixed by `^id`, but we can add the id in the next line, with no space prefix to it.

<<wikitext-example src:"```css
.main {
display: none;
}
```

^BlockLevelLinksID2

">>

! Link to the block ID ^091607

Adding `^blockID` after the title in the link, will make this link highlight the block with that ID.

<<wikitext-example-without-html src:"[[Link to BlockLevelLinksID1|Block Level Links in WikiText^BlockLevelLinksID1]]">>

<<wikitext-example-without-html src:"[[Link to BlockLevelLinksID2|Block Level Links in WikiText^BlockLevelLinksID2]]">>

<<wikitext-example src:"[[Link to Title|Block Level Links in WikiText^🤗→AddingIDforblock]]">>
8 changes: 7 additions & 1 deletion editions/tw5.com/tiddlers/wikitext/Linking in WikiText.tid
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
caption: Linking
created: 20131205155230596
modified: 20211230145939554
modified: 20230917121659927
tags: WikiText
title: Linking in WikiText
type: text/vnd.tiddlywiki
Expand Down Expand Up @@ -123,3 +123,9 @@ See also another example of [[constructing dynamic links|Concatenating text and
In TiddlyWiki anchor links can help us link to target points and distinct sections within rendered tiddlers. They can help the reader navigate longer tiddler content.

See [[Anchor Links using HTML]] for more information.

! Linking within tiddlers - Link to block

You can link to a specific block within a tiddler using `^blockId` syntax. You will also get block level backlinks with this technique. Some examples are in [[BlockIdWidget^exampleid1]].

See [[Block Level Links in WikiText^091607]] for more information.
Loading
Loading