Skip to content

Instantly share code, notes, and snippets.

@thesved
Last active August 10, 2021 07:43
Show Gist options
  • Save thesved/79371d0c1dd34b6750c846368b323113 to your computer and use it in GitHub Desktop.
Save thesved/79371d0c1dd34b6750c846368b323113 to your computer and use it in GitHub Desktop.
Copy this to roam/js page, including the "{{[[roam/js]]}}" node:
- {{[[roam/js]]}}
- ```javascript
/*
* Roam template PoC by @ViktorTabori
* 0.1alpha
*
* How to install it:
* - go to `roam/js` page`
* - make a new node: {{[[roam/js]]}}
* - put this code under that node
* - set type to javascript and allow the js to run
* - create a template page with some content: [[template]]/test
* - write :test: to you daily page and see what happens
*
* known issues:
* - looks hacky
* - for longer templates it messes up some lines
*/
document.addEventListener('input', function(e){
if ('_templateHook' in window) {
setTimeout(function(){ window._templateHook(e); }, 0);
}
});
window._templateHook = async function(e) {
// logging
window._e = e;
// exit if not target
var elem = e.target
if (elem.nodeName != 'TEXTAREA' || e.data != ':') return;
console.log('ok',elem.value, elem);
// nativeValueSetter to bypass
var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,"value").set;
// resolve templates
var tab = 0;
var text = elem.value;
elem.value.replace(/:([^:]+):/g, async function(_, v, position){
// lookup template
var tmp = getTemplate(v);
// if no result
if (!tmp) {
return _;
}
console.log('template:',v,tmp);
// remove first
tmp = tmp.replace(/^\s*- /,'').split("\n");
// process first line
var line = tmp.shift();
text = text.replace(_, line);
// handle heading
heading = (line.match(/^(#*) ?/)||['',''])[1].length;
if (heading > 0) {
console.log('heading:', heading, line.match(/^(#*) ?/));
KeyboardLib.changeHeading(heading);
}
line = line.replace(/^#* ?/, '');
if (line == '') line = ' ';
// set value
nativeSetter.call(e.target, line);
e.target.selectionStart = position;
e.target.selectionEnd = position;
e.target.dispatchEvent(new Event('input', {bubbles: true, cancelable: true }));
// process lines
while (tmp.length) {
// get new line
elem.focus();
elem.selectionStart = elem.value.length;
elem.selectionEnd = elem.value.length;
// get new line and row
await KeyboardLib.pressEnter();
elem = KeyboardLib.getActiveEditElement();
line = tmp.shift();
// handle tabs
console.log('line:', line)
tab = line.match(/^\s*/)[0].length/2-tab; // tab difference
console.log('tab:',tab);
if (tab > 0) {
for (var i=0; i<tab; i++) {
await KeyboardLib.pressTab()
}
} else if (tab < 0) {
for (var i=0; i<-tab; i++) {
await KeyboardLib.pressShiftTab();
}
}
tab = line.match(/^\s*/)[0].length/2; // save current tab length
// handle heading
heading = (line.match(/^\s*- (#*) ?/)||['',''])[1].length;
if (heading > 0) {
console.log('heading:', heading, line.match(/^\s*- (#*) ?/));
KeyboardLib.changeHeading(heading);
}
// set element value
elem = KeyboardLib.getActiveEditElement();
line = line.replace(/^\s*- #* ?/,'');
if (line == '') line = ' ';
nativeSetter.call(elem, line);
elem.selectionStart = elem.value.length;
elem.selectionEnd = elem.value.length;
console.log('dispatch event');
elem.dispatchEvent(new Event('input', {bubbles: true, cancelable: true }));
await KeyboardLib.delay(150);
}
});
}
window.getTemplate = function(name) {
/* resolve node function by @ViktorTabori
* id: node id
* level: depth needed for indention
* trail: list of ids to avoid loops
* resolve: resolve block references and embeds starting with an exclamation mark: !{{embed:((blockid))}} and !((blockid))
* skipFirstPrefix: no prefix, needed for block embeds and references
* stop: doesn't resolve children, needed for block reference resolution
*/
function resolveNode(id, level, trail, resolve, skipFirstPrefix, stop){
var level = level || 0; // for indentation
var trail = Object.assign({}, trail); // to avoid loops
var prefix = skipFirstPrefix ? '' : ' '.repeat(2*Math.max(level-1,0)) + '- '; // indention starting from level 2
var newLine = skipFirstPrefix && stop ? '' : "\n"; // no new line when we resolve simple block references
var ret = '';
// avoid loops: skip if trail already contains id
if (trail[id]) return;
trail[id] = true;
// get node info
var node = window.roamAlphaAPI.pull("[*]",id);
// node order
var order = node[':block/order'] || 0;
// add heading to prefix
if (node[':block/heading'] && node[':block/heading'] > 0) {
prefix += '#'.repeat(node[':block/heading'])+' ';
}
// current node string
if (typeof node[':block/string'] != 'undefined') {
// resolve block EMBEDs
var regexEmbed = resolve ? /!?{{\[*embed\]*\s*:\s*\(\(([^\)]*)\)\)\s*}}/ig : /!{{\[*embed\]*\s*:\s*\(\(([^\)]*)\)\)\s*}}/ig;
node[':block/string'] = node[':block/string'].replace(regexEmbed, function(_, v){
var uid = v.trim();
var id = window.roamAlphaAPI.q("[:find ?e :in $ ?a :where [?e :block/uid ?a]]", uid);
if (id.length == 0) {
return _;
}
var block = resolveNode(id[0][0], level, trail, true, true); // resolve node, no prefix
if (typeof block != 'undefined') { // for loops we got back undefined
return block;
} else {
return 'LOOP:'+_;
}
});
// resolve block REFERENCEs
var regexReference = resolve ? /!?\(\(([^\)]*)\)\)/ig : /!\(\(([^\)]*)\)\)/ig;
node[':block/string'] = node[':block/string'].replace(regexReference, function(_, v){
var uid = v.trim();
var id = window.roamAlphaAPI.q("[:find ?e :in $ ?a :where [?e :block/uid ?a]]", uid);
if (id.length == 0) {
return _;
}
var block = resolveNode(id[0][0], level, trail, true, true, true); // resolve node, no prefix, don't resolve children
if (typeof block != 'undefined') { // for loops we got back undefined
return block;
} else {
return 'LOOP:'+_;
}
});
// add block text to return
ret += prefix + node[':block/string'] + newLine;
}
// handle children
if (node[':block/children'] && !stop) {
var children = [];
var tmp;
// get children data
for (var i in node[':block/children']) {
tmp = resolveNode(node[':block/children'][i][':db/id'], level+1, trail);
if (typeof tmp != 'undefined') {
children.push(tmp);
}
}
// sort children in order
children.sort(function(a,b){return a.order-b.order})
// concat children text
ret += children.map(function(i){return i.txt}).join('');
}
// return based on how deep we are in the graph
if (level == 0 || skipFirstPrefix) {
return ret;
} else {
return {txt: ret, order: order};
}
}
// check API endpoint
if (!window.roamAlphaAPI || !window.roamAlphaAPI.q || !window.roamAlphaAPI.pull) return; // no api endpoint
// search node ID
var nodeId; // page we look for
var search = ['template','[[template]]']; // search for template in template/name, [[template]]/name, ...
for (var i in search) {
nodeId = window.roamAlphaAPI.q("[:find ?e :in $ ?a :where [?e :node/title ?a]]", search[i]+'/'+name);
if (nodeId.length) {
nodeId = nodeId[0][0];
break;
}
}
if (!nodeId || nodeId.length == 0) return; // no such template
return resolveNode(nodeId);
}
window.KeyboardLib = {
// thank you @VladyslavSitalo for the awesome Roam Toolkit, and the basis for this code
LEFT_ARROW: 37,
UP_ARROW: 38,
RIGHT_ARROW: 39,
DOWN_ARROW: 40,
BASE_DELAY: 20,
delay(millis) {
return new Promise(resolve => setTimeout(resolve, millis))
},
getKeyboardEvent: function(type, code, opts) {
return new KeyboardEvent(type, {
bubbles: true,
cancelable: true,
keyCode: code,
...opts,
})
},
getActiveEditElement: function() {
// stolen from Surfingkeys. Needs work.
var element = document.activeElement
// on some pages like chrome://history/, input is in shadowRoot of several other recursive shadowRoots.
while (element.shadowRoot) {
if (element.shadowRoot.activeElement) {
element = element.shadowRoot.activeElement
} else {
var subElement = element.shadowRoot.querySelector('input, textarea, select')
if (subElement) {
element = subElement
}
break
}
}
return element
},
async simulateSequence(events, delayOverride) {
;events.forEach(function(e){
return KeyboardLib.getActiveEditElement().dispatchEvent(KeyboardLib.getKeyboardEvent(e.name, e.code, e.opt));
});
return this.delay(delayOverride || this.BASE_DELAY);
},
async simulateKey(code, delayOverride, opts) {
return this.simulateSequence([{name:'keydown', code:code, opt:opts}, {name:'keyup', code:code, opt:opts}], delayOverride);
},
async changeHeading(heading, delayOverride) {
return this.simulateSequence(
[
{name:'keydown', code:18, opt:{altKey:true}},
{name:'keydown', code:91, opt:{metaKey:true}},
{name:'keydown', code:48+heading, opt:{altKey:true, metaKey:true}},
{name:'keyup', code:91, opt:{altKey:true}},
{name:'keyup', code:18, opt:{}}
],
delayOverride);
},
async pressEnter(delayOverride) {
return this.simulateKey(13, delayOverride)
},
async pressEsc(delayOverride) {
return this.simulateKey(27, delayOverride)
},
async pressBackspace(delayOverride) {
return this.simulateKey(8, delayOverride)
},
async pressTab(delayOverride) {
return this.simulateKey(9, delayOverride)
},
async pressShiftTab(delayOverride) {
return this.simulateKey(9, delayOverride, {shiftKey: true})
},
async pressCtrlV(delayOverride) {
return this.simulateKey(118, delayOverride, {metaKey: true})
},
getInputEvent() {
return new Event('input', {
bubbles: true,
cancelable: true,
})
},
}```
@b1indsight
Copy link

b1indsight commented Aug 8, 2020

Thank you for this template! It helps me a lot in writing my daliy note!
And I find a small bug in handling heading part. when I add a Tag like '#dosomething' to the start of a line. The value of this line will be
'- #dosomething'. So the statement in line 113 (line = line.replace(/^\s*- #* ?/,'');) will match '- #' in the value and replace it by ''.

I solve this bug by changing all the regex string which used to handle heading to /^\s*- #* +?/. I use statements line = line.replace(/^\s*- /,'');line = line.replace(/(#+) +?/,''); to replace the statement in line 113. Maybe this solution will give you some help.

@bzupnick
Copy link

Thank you for this template! It helps me a lot in writing my daliy note!
And I find a small bug in handling heading part. when I add a Tag like '#dosomething' to the start of a line. The value of this line will be
'- #dosomething'. So the statement in line 113 (line = line.replace(/^\s*- #* ?/,'');) will match '- #' in the value and replace it by ''.

I solve this bug by changing all the regex string which used to handle heading to /^\s*- #* +?/. I use statements line = line.replace(/^\s*- /,'');line = line.replace(/(#+) +?/,''); to replace the statement in line 113. Maybe this solution will give you some help.

I'm experiencing this bug not just if it's the first line but any use of # as opposed to [[]] seems to break the script. Does your fix fix that too?

@gaa23
Copy link

gaa23 commented Aug 15, 2020

this isn't working for me

@williamvz
Copy link

williamvz commented Aug 17, 2020

It took me a while but it looks like I have the script up and running. However, the templates do not carry over nicely as of now. Even three flat lines make one line disappear. Not sure how to get onto this one but really like the idea!

@TechplexEngineer
Copy link

I found that if I have a line with tags in it, I have to ensure there is a space after the last tag.
If the line looks like this labels:: #awesome the line does not show up
But this does work:
labels:: #awesome (there is a space after the e in awesome)

@hugiselephant
Copy link

found this page through this Youtube video. Nice demo of how this plugin works. - Works great for me. love it thanks.

https://www.youtube.com/watch?v=aJEqN98j2W4

@hugiselephant
Copy link

Would it be possible to have it insert text instead of replace the entire line?

for instance:

As I type something :template:

This would produce

As I type something **text gets added here**

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment