Work on inline attributes
parent
a1c623b1f5
commit
2b494f263e
|
@ -18,3 +18,7 @@ export const DirectiveTag = Tag.define();
|
||||||
export const DirectiveStartTag = Tag.define();
|
export const DirectiveStartTag = Tag.define();
|
||||||
export const DirectiveEndTag = Tag.define();
|
export const DirectiveEndTag = Tag.define();
|
||||||
export const DirectiveProgramTag = Tag.define();
|
export const DirectiveProgramTag = Tag.define();
|
||||||
|
|
||||||
|
export const AttributeTag = Tag.define();
|
||||||
|
export const AttributeNameTag = Tag.define();
|
||||||
|
export const AttributeValueTag = Tag.define();
|
||||||
|
|
|
@ -89,3 +89,16 @@ Deno.test("Test directive parser", () => {
|
||||||
tree = parse(lang, orderByExample);
|
tree = parse(lang, orderByExample);
|
||||||
console.log("Tree", JSON.stringify(tree, null, 2));
|
console.log("Tree", JSON.stringify(tree, null, 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inlineAttributeSample = `
|
||||||
|
Hello there [a link](http://zef.plus) and [age:: 100]
|
||||||
|
`;
|
||||||
|
|
||||||
|
Deno.test("Test inline attribute syntax", () => {
|
||||||
|
const lang = buildMarkdown([]);
|
||||||
|
const tree = parse(lang, inlineAttributeSample);
|
||||||
|
const nameNode = findNodeOfType(tree, "AttributeName");
|
||||||
|
assertEquals(nameNode?.children![0].text, "age");
|
||||||
|
const valueNode = findNodeOfType(tree, "AttributeValue");
|
||||||
|
assertEquals(valueNode?.children![0].text, "100");
|
||||||
|
});
|
||||||
|
|
|
@ -142,6 +142,55 @@ export const Highlight: MarkdownConfig = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const attributeRegex = /^\[([^:]+)(::\s*)([^\]]+)\]/;
|
||||||
|
|
||||||
|
export const Attribute: MarkdownConfig = {
|
||||||
|
defineNodes: [
|
||||||
|
{ name: "Attribute", style: { "Attribute/...": ct.AttributeTag } },
|
||||||
|
{ name: "AttributeName", style: ct.AttributeNameTag },
|
||||||
|
{ name: "AttributeValue", style: ct.AttributeValueTag },
|
||||||
|
{ name: "AttributeMark", style: t.processingInstruction },
|
||||||
|
{ name: "AttributeColon", style: t.processingInstruction },
|
||||||
|
],
|
||||||
|
parseInline: [
|
||||||
|
{
|
||||||
|
name: "Attribute",
|
||||||
|
parse(cx, next, pos) {
|
||||||
|
let match: RegExpMatchArray | null;
|
||||||
|
if (
|
||||||
|
next != 91 /* '[' */ ||
|
||||||
|
// and match the whole thing
|
||||||
|
!(match = attributeRegex.exec(cx.slice(pos, cx.end)))
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const [fullMatch, attributeName, attributeColon, attributeValue] =
|
||||||
|
match;
|
||||||
|
const endPos = pos + fullMatch.length;
|
||||||
|
|
||||||
|
return cx.addElement(
|
||||||
|
cx.elt("Attribute", pos, endPos, [
|
||||||
|
cx.elt("AttributeMark", pos, pos + 1), // [
|
||||||
|
cx.elt("AttributeName", pos + 1, pos + 1 + attributeName.length),
|
||||||
|
cx.elt(
|
||||||
|
"AttributeColon",
|
||||||
|
pos + 1 + attributeName.length,
|
||||||
|
pos + 1 + attributeName.length + attributeColon.length,
|
||||||
|
),
|
||||||
|
cx.elt(
|
||||||
|
"AttributeValue",
|
||||||
|
pos + 1 + attributeName.length + attributeColon.length,
|
||||||
|
endPos - 1,
|
||||||
|
),
|
||||||
|
cx.elt("AttributeMark", endPos - 1, endPos), // [
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
after: "Emphasis",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
class CommentParser implements LeafBlockParser {
|
class CommentParser implements LeafBlockParser {
|
||||||
nextLine() {
|
nextLine() {
|
||||||
return false;
|
return false;
|
||||||
|
@ -343,6 +392,7 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language {
|
||||||
extensions: [
|
extensions: [
|
||||||
WikiLink,
|
WikiLink,
|
||||||
CommandLink,
|
CommandLink,
|
||||||
|
Attribute,
|
||||||
FrontMatter,
|
FrontMatter,
|
||||||
Directive,
|
Directive,
|
||||||
TaskList,
|
TaskList,
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
||||||
|
import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||||
|
import { extractAttributes } from "$sb/lib/attribute.ts";
|
||||||
|
import { assertEquals } from "../../test_deps.ts";
|
||||||
|
import { renderToText } from "$sb/lib/tree.ts";
|
||||||
|
|
||||||
|
const inlineAttributeSample = `
|
||||||
|
# My document
|
||||||
|
Top level attributes: [name:: sup] [age:: 42]
|
||||||
|
|
||||||
|
* [ ] Attribute in a task [tag:: foo]
|
||||||
|
* Regular item [tag:: bar]
|
||||||
|
|
||||||
|
1. Itemized list [tag:: baz]
|
||||||
|
`;
|
||||||
|
|
||||||
|
const cleanedInlineAttributeSample = `
|
||||||
|
# My document
|
||||||
|
Top level attributes:
|
||||||
|
|
||||||
|
* [ ] Attribute in a task [tag:: foo]
|
||||||
|
* Regular item [tag:: bar]
|
||||||
|
|
||||||
|
1. Itemized list [tag:: baz]
|
||||||
|
`;
|
||||||
|
|
||||||
|
Deno.test("Test attribute extraction", () => {
|
||||||
|
const lang = buildMarkdown([]);
|
||||||
|
const tree = parse(lang, inlineAttributeSample);
|
||||||
|
const toplevelAttributes = extractAttributes(tree, false);
|
||||||
|
assertEquals(Object.keys(toplevelAttributes).length, 2);
|
||||||
|
assertEquals(toplevelAttributes.name, "sup");
|
||||||
|
assertEquals(toplevelAttributes.age, 42);
|
||||||
|
// Check if the attributes are still there
|
||||||
|
assertEquals(renderToText(tree), inlineAttributeSample);
|
||||||
|
// Now once again with cleaning
|
||||||
|
extractAttributes(tree, true);
|
||||||
|
assertEquals(renderToText(tree), cleanedInlineAttributeSample);
|
||||||
|
});
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
findNodeOfType,
|
||||||
|
ParseTree,
|
||||||
|
replaceNodesMatching,
|
||||||
|
} from "$sb/lib/tree.ts";
|
||||||
|
|
||||||
|
export type Attribute = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const numberRegex = /^-?\d+(\.\d+)?$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts attributes from a tree, optionally cleaning them out of the tree.
|
||||||
|
* @param tree tree to extract attributes from
|
||||||
|
* @param clean whether or not to clean out the attributes from the tree
|
||||||
|
* @returns mapping from attribute name to attribute value
|
||||||
|
*/
|
||||||
|
export function extractAttributes(
|
||||||
|
tree: ParseTree,
|
||||||
|
clean: boolean,
|
||||||
|
): Record<string, any> {
|
||||||
|
const attributes: Record<string, any> = {};
|
||||||
|
replaceNodesMatching(tree, (n) => {
|
||||||
|
if (n.type === "ListItem") {
|
||||||
|
// Find top-level only, no nested lists
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
if (n.type === "Attribute") {
|
||||||
|
const nameNode = findNodeOfType(n, "AttributeName");
|
||||||
|
const valueNode = findNodeOfType(n, "AttributeValue");
|
||||||
|
if (nameNode && valueNode) {
|
||||||
|
let val: any = valueNode.children![0].text!;
|
||||||
|
if (numberRegex.test(val)) {
|
||||||
|
val = +val;
|
||||||
|
}
|
||||||
|
attributes[nameNode.children![0].text!] = val;
|
||||||
|
}
|
||||||
|
// Remove from tree
|
||||||
|
if (clean) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Go on...
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
return attributes;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||||
import { collectNodesOfType, ParseTree, renderToText } from "$sb/lib/tree.ts";
|
import { collectNodesOfType, ParseTree, renderToText } from "$sb/lib/tree.ts";
|
||||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||||
|
import { extractAttributes } from "$sb/lib/attribute.ts";
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -11,7 +12,7 @@ export type Item = {
|
||||||
// Not stored in DB
|
// Not stored in DB
|
||||||
page?: string;
|
page?: string;
|
||||||
pos?: number;
|
pos?: number;
|
||||||
};
|
} & Record<string, any>;
|
||||||
|
|
||||||
export async function indexItems({ name, tree }: IndexTreeEvent) {
|
export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||||
const items: { key: string; value: Item }[] = [];
|
const items: { key: string; value: Item }[] = [];
|
||||||
|
@ -30,6 +31,10 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const item: Item = {
|
||||||
|
name: "", // to be replaced
|
||||||
|
};
|
||||||
|
|
||||||
const textNodes: ParseTree[] = [];
|
const textNodes: ParseTree[] = [];
|
||||||
let nested: string | undefined;
|
let nested: string | undefined;
|
||||||
for (const child of n.children!.slice(1)) {
|
for (const child of n.children!.slice(1)) {
|
||||||
|
@ -37,13 +42,15 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||||
nested = renderToText(child);
|
nested = renderToText(child);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Extract attributes and remove from tree
|
||||||
|
const extractedAttributes = extractAttributes(child, true);
|
||||||
|
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||||
|
item[key] = value;
|
||||||
|
}
|
||||||
textNodes.push(child);
|
textNodes.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemText = textNodes.map(renderToText).join("").trim();
|
item.name = textNodes.map(renderToText).join("").trim();
|
||||||
const item: Item = {
|
|
||||||
name: itemText,
|
|
||||||
};
|
|
||||||
if (nested) {
|
if (nested) {
|
||||||
item.nested = nested;
|
item.nested = nested;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { applyQuery } from "$sb/lib/query.ts";
|
||||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
import { invokeFunction } from "$sb/silverbullet-syscall/system.ts";
|
import { invokeFunction } from "$sb/silverbullet-syscall/system.ts";
|
||||||
import { isValidPageName } from "$sb/lib/page.ts";
|
import { isValidPageName } from "$sb/lib/page.ts";
|
||||||
|
import { extractAttributes } from "$sb/lib/attribute.ts";
|
||||||
|
|
||||||
// Key space:
|
// Key space:
|
||||||
// l:toPage:pos => {name: pageName, inDirective: true}
|
// l:toPage:pos => {name: pageName, inDirective: true}
|
||||||
|
@ -42,14 +43,21 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||||
// [[Style Links]]
|
// [[Style Links]]
|
||||||
// console.log("Now indexing links for", name);
|
// console.log("Now indexing links for", name);
|
||||||
const pageMeta = await extractFrontmatter(tree);
|
const pageMeta = await extractFrontmatter(tree);
|
||||||
if (Object.keys(pageMeta).length > 0) {
|
const toplevelAttributes = extractAttributes(tree, false);
|
||||||
// console.log("Extracted page meta data", pageMeta);
|
if (
|
||||||
|
Object.keys(pageMeta).length > 0 ||
|
||||||
|
Object.keys(toplevelAttributes).length > 0
|
||||||
|
) {
|
||||||
|
for (const [k, v] of Object.entries(toplevelAttributes)) {
|
||||||
|
pageMeta[k] = v;
|
||||||
|
}
|
||||||
// Don't index meta data starting with $
|
// Don't index meta data starting with $
|
||||||
for (const key in pageMeta) {
|
for (const key in pageMeta) {
|
||||||
if (key.startsWith("$")) {
|
if (key.startsWith("$")) {
|
||||||
delete pageMeta[key];
|
delete pageMeta[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log("Extracted page meta data", pageMeta);
|
||||||
await index.set(name, "meta:", pageMeta);
|
await index.set(name, "meta:", pageMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ type MarkdownRenderOptions = {
|
||||||
smartHardBreak?: true;
|
smartHardBreak?: true;
|
||||||
annotationPositions?: true;
|
annotationPositions?: true;
|
||||||
attachmentUrlPrefix?: string;
|
attachmentUrlPrefix?: string;
|
||||||
|
preserveAttributes?: true;
|
||||||
// When defined, use to inline images as data: urls
|
// When defined, use to inline images as data: urls
|
||||||
translateUrls?: (url: string) => string;
|
translateUrls?: (url: string) => string;
|
||||||
};
|
};
|
||||||
|
@ -345,6 +346,17 @@ function render(
|
||||||
const body = findNodeOfType(t, "DirectiveBody")!;
|
const body = findNodeOfType(t, "DirectiveBody")!;
|
||||||
return posPreservingRender(body.children![0], options);
|
return posPreservingRender(body.children![0], options);
|
||||||
}
|
}
|
||||||
|
case "Attribute":
|
||||||
|
if (options.preserveAttributes) {
|
||||||
|
return {
|
||||||
|
name: "span",
|
||||||
|
attrs: {
|
||||||
|
class: "attribute",
|
||||||
|
},
|
||||||
|
body: renderToText(t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
// Text
|
// Text
|
||||||
case undefined:
|
case undefined:
|
||||||
return t.text!;
|
return t.text!;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from "$sb/lib/tree.ts";
|
} from "$sb/lib/tree.ts";
|
||||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||||
import { niceDate } from "$sb/lib/dates.ts";
|
import { niceDate } from "$sb/lib/dates.ts";
|
||||||
|
import { extractAttributes } from "$sb/lib/attribute.ts";
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -33,7 +34,7 @@ export type Task = {
|
||||||
// Not saved in DB, just added when pulled out (from key)
|
// Not saved in DB, just added when pulled out (from key)
|
||||||
pos?: number;
|
pos?: number;
|
||||||
page?: string;
|
page?: string;
|
||||||
};
|
} & Record<string, any>;
|
||||||
|
|
||||||
function getDeadline(deadlineNode: ParseTree): string {
|
function getDeadline(deadlineNode: ParseTree): string {
|
||||||
return deadlineNode.children![0].text!.replace(/📅\s*/, "");
|
return deadlineNode.children![0].text!.replace(/📅\s*/, "");
|
||||||
|
@ -67,6 +68,12 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extract attributes and remove from tree
|
||||||
|
const extractedAttributes = extractAttributes(n, true);
|
||||||
|
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||||
|
task[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
task.name = n.children!.slice(1).map(renderToText).join("").trim();
|
task.name = n.children!.slice(1).map(renderToText).join("").trim();
|
||||||
|
|
||||||
const taskIndex = n.parent!.children!.indexOf(n);
|
const taskIndex = n.parent!.children!.indexOf(n);
|
||||||
|
|
|
@ -43,6 +43,7 @@ class TableViewWidget extends WidgetType {
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
preserveAttributes: true,
|
||||||
});
|
});
|
||||||
return dom;
|
return dom;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ export default function highlightStyles(mdExtension: MDExt[]) {
|
||||||
{ tag: ct.WikiLinkPageTag, class: "sb-wiki-link-page" },
|
{ tag: ct.WikiLinkPageTag, class: "sb-wiki-link-page" },
|
||||||
{ tag: ct.CommandLinkTag, class: "sb-command-link" },
|
{ tag: ct.CommandLinkTag, class: "sb-command-link" },
|
||||||
{ tag: ct.CommandLinkNameTag, class: "sb-command-link-name" },
|
{ tag: ct.CommandLinkNameTag, class: "sb-command-link-name" },
|
||||||
|
{ tag: ct.AttributeTag, class: "sb-frontmatter" },
|
||||||
|
{ tag: ct.AttributeNameTag, class: "sb-atom" },
|
||||||
{ tag: ct.TaskTag, class: "sb-task" },
|
{ tag: ct.TaskTag, class: "sb-task" },
|
||||||
{ tag: ct.TaskMarkerTag, class: "sb-task-marker" },
|
{ tag: ct.TaskMarkerTag, class: "sb-task-marker" },
|
||||||
{ tag: ct.CodeInfoTag, class: "sb-code-info" },
|
{ tag: ct.CodeInfoTag, class: "sb-code-info" },
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
font-family: var(--editor-font);
|
font-family: var(--editor-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-notifications > div {
|
.sb-notifications>div {
|
||||||
border: var(--notifications-border-color) 1px solid;
|
border: var(--notifications-border-color) 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +185,7 @@
|
||||||
background-color: var(--editor-command-button-background-color);
|
background-color: var(--editor-command-button-background-color);
|
||||||
border: 1px solid var(--editor-command-button-border-color);
|
border: 1px solid var(--editor-command-button-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-command-button:hover {
|
.sb-command-button:hover {
|
||||||
background-color: var(--editor-command-button-hover-background-color);
|
background-color: var(--editor-command-button-hover-background-color);
|
||||||
}
|
}
|
||||||
|
@ -193,11 +194,13 @@
|
||||||
&.sb-meta {
|
&.sb-meta {
|
||||||
color: var(--editor-command-button-meta-color);
|
color: var(--editor-command-button-meta-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sb-command-link-name {
|
&.sb-command-link-name {
|
||||||
background-color: var(--editor-command-button-color);
|
background-color: var(--editor-command-button-color);
|
||||||
background-color: var(--editor-command-button-background-color);
|
background-color: var(--editor-command-button-background-color);
|
||||||
border: 1px solid var(--editor-command-button-border-color);
|
border: 1px solid var(--editor-command-button-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sb-command-link-name:hover {
|
&.sb-command-link-name:hover {
|
||||||
background-color: var(--editor-command-button-hover-background-color);
|
background-color: var(--editor-command-button-hover-background-color);
|
||||||
}
|
}
|
||||||
|
@ -209,7 +212,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Then undo other meta */
|
/* Then undo other meta */
|
||||||
.sb-line-li .sb-meta ~ .sb-meta {
|
.sb-line-li .sb-meta~.sb-meta {
|
||||||
color: var(--editor-meta-color);
|
color: var(--editor-meta-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,7 +329,7 @@
|
||||||
|
|
||||||
.sb-directive-start-outside,
|
.sb-directive-start-outside,
|
||||||
.sb-directive-end-outside {
|
.sb-directive-end-outside {
|
||||||
& > span.sb-directive-placeholder {
|
&>span.sb-directive-placeholder {
|
||||||
color: var(--editor-directive-info-color);
|
color: var(--editor-directive-info-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,7 +369,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a.sb-wiki-link-page-missing,
|
a.sb-wiki-link-page-missing,
|
||||||
.sb-wiki-link-page-missing > .sb-wiki-link-page {
|
.sb-wiki-link-page-missing>.sb-wiki-link-page {
|
||||||
color: var(--editor-wiki-link-page-missing-color);
|
color: var(--editor-wiki-link-page-missing-color);
|
||||||
background-color: var(--editor-wiki-link-page-background-color);
|
background-color: var(--editor-wiki-link-page-background-color);
|
||||||
}
|
}
|
||||||
|
@ -380,8 +383,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-line-comment {
|
.sb-line-comment {
|
||||||
background-color: var(
|
background-color: var(--editor-code-comment-color); // rgba(255, 255, 0, 0.5);
|
||||||
--editor-code-comment-color
|
|
||||||
); // rgba(255, 255, 0, 0.5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue