diff --git a/README.md b/README.md index 2560243..505c4c2 100644 --- a/README.md +++ b/README.md @@ -10,27 +10,30 @@ Consider the following example: you have daily/weekly notes where you jolt down in `2026-01-01.md` you have the following: ```markdown -- [[MyArea]] - - [[MyProject]] - - https://my-project-reference #bookmark +- [[Project1]] + - [An external link to remember](https://company-reference) #bookmark ``` -in another day, you decide you want to add a note to `My Project` in `2026-01-02.md`. Most natural way to do so in Obsidian -is to quickly nest it in your otline +As your work progresses you add more notes, nested with a hierarchy that evolves as your work evolves. +Obsidian is good at linking these notes together easily. You start adding more important links ```markdown -- [[MyProject]] - - [[MyProject Task]] with some optional inline context +- [[Project1]] + - [[Project1 Task]] with some optional inline context + - https://task-tracker/item - this is something I should keep track of temporarily + +- [[Project2]] + - https://my-project-himepage #bookmark/important ``` -yet another day comes and you start ading some references tot the area you're working on in `2026-01-03.md` +Not let's decide you want to start organizing a bit and organize your more important projects [[ImportantProjects]] page ```markdown -- [[MyArea]] - - https://some-web-reference #bookmark/important +- [[Project1]] +- [[Project2]] ``` -TreeSearch let's explore these connections quickly and easily. +TreeSearch let's search and explore these connections across your vault, easily. ![img.png](docs/img/img.png) @@ -39,6 +42,9 @@ TreeSearch let's explore these connections quickly and easily. The plugin adds a new command `TreeSearch: Search` that opens a new pane with a search bar. You can search for a note and see all the notes (and links) that are nested under it. +The search algo is very simple and only supports exact matches for now. There are only two operators you should care about: +- `>` - searches down in the tee + ## Funding URL You can include funding URLs where people who use your plugin can financially support it. diff --git a/package-lock.json b/package-lock.json index 7a03c7b..b3b06c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "react-markdown": "^9.0.1" }, "devDependencies": { - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/markdown-it": "^14.1.2", "@types/node": "^16.11.6", "@types/react": "^18.3.5", diff --git a/package.json b/package.json index c634bb2..e8e7442 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "author": "", "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/markdown-it": "^14.1.2", "@types/node": "^16.11.6", "@types/react": "^18.3.5", diff --git a/src/obsidian-utils.ts b/src/obsidian-utils.ts index 53685de..56f85f8 100644 --- a/src/obsidian-utils.ts +++ b/src/obsidian-utils.ts @@ -1,6 +1,6 @@ import { App } from "obsidian"; -export async function openFileAndHighlightLine(app: App, path: string, lineNumber: number) { +export async function openFileAndHighlightLine(app: App, path: string, start: { line: number, col: number }, end: { line: number, col: number }) { const file = app.vault.getAbstractFileByPath(path); if (file) { const leaf = this.app.workspace.getLeaf(); @@ -9,9 +9,9 @@ export async function openFileAndHighlightLine(app: App, path: string, lineNumbe // Set timeout to ensure the file is loaded setTimeout(() => { const editor = this.app.workspace.activeLeaf.view.sourceMode.cmEditor as CodeMirror.Editor; - editor.setCursor(lineNumber, 0); + editor.setCursor(start.line, 0); editor.focus(); - editor.setSelection({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: 40 }); + editor.setSelection({ line: start.line, ch: start.col }, { line: end.line, ch: end.col }); }, 100); } - } \ No newline at end of file + } diff --git a/src/query.ts b/src/query.ts new file mode 100644 index 0000000..69754e6 --- /dev/null +++ b/src/query.ts @@ -0,0 +1,55 @@ +export type SearchExpr = { + operator: "and" | "or" | "not" | "value", + children: SearchExpr[], + value: string +}; + +function matchesExpr(text: string, expr: SearchExpr): boolean { + if (expr.operator === "and") { + return expr.children.every(c => matchesExpr(text, c)); + } else if (expr.operator === "or") { + return expr.children.some(c => matchesExpr(text, c)); + } else if (expr.operator === "not") { + return !matchesExpr(text, expr.children[0]); + } else { + return text.includes(expr.value); + } +} + +export function parseQuery(query: string): SearchExpr { + const words = query.toLowerCase().split(" ") + .map(it => it.trim()) + .filter(it => it.length > 0); + const stack: SearchExpr[] = []; + + for (const word of words) { + if (word === "and") { + stack.push({ operator: "and", children: [], value: "" }); + } else if (word === "or") { + stack.push({ operator: "or", children: [], value: "" }); + } else if (word.startsWith("-")) { + stack.push({ operator: "not", children: [{ operator: "value", children: [], value: word.substring(1) }], value: "" }); + } else { + stack.push({ operator: "value", children: [], value: word }); + } + } + + const root: SearchExpr = { operator: "and", children: [], value: "" }; + let current: SearchExpr = root; + + for (const node of stack) { + if (node.operator === "and" || node.operator === "or") { + const newNode: SearchExpr = { operator: node.operator, children: [], value: "" }; + current.children.push(newNode); + current = newNode; + } else { + current.children.push(node); + } + } + + return root; +} + +export function matchQuery(text: string, parsedQuery: SearchExpr): boolean { + return matchesExpr(text, parsedQuery); +} diff --git a/src/search.ts b/src/search.ts index 1bb98de..a44ec18 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,5 +1,6 @@ -import {EdgeAttributes, GraphAttributes, Index, NodeAttributes} from "./tree-builder"; +import {DirectedGraphOfNotes, EdgeAttributes, GraphAttributes, Index, NodeAttributes} from "./tree-builder"; import Graph from "graphology"; +import {matchQuery, parseQuery, SearchExpr} from "./query"; export type ResultNode = { value: string, @@ -8,20 +9,18 @@ export type ResultNode = { parents: string[] } -type GF = Graph - -function filterTreeByWord(node: ResultNode, word: string): ResultNode | null { - // Check if the current node contains the word - if (node.value.includes(word)) { +function filterTreeByWord(node: ResultNode, expr: SearchExpr): ResultNode | null { + // Check if the current node contains the expr + if (matchQuery(node.value, expr)) { return node; } // Recursively filter the children const filteredChildren = node.children - .map(child => filterTreeByWord(child, word)) + .map(child => filterTreeByWord(child, expr)) .filter(child => child !== null) as ResultNode[]; - // If no children contain the word, return null + // If no children contain the expr, return null if (filteredChildren.length === 0) { return null; } @@ -33,19 +32,19 @@ function filterTreeByWord(node: ResultNode, word: string): ResultNode | null { }; } -function filterDown(results: ResultNode[], words: string[]): ResultNode[] { - if (words.length === 0) return results +function filterDown(results: ResultNode[], search: SearchExpr[]): ResultNode[] { + if (search.length === 0) return results - const word = words[0] + const expr = search[0] const filtered = results - .map(r => filterTreeByWord(r, word)) + .map(r => filterTreeByWord(r, expr)) .filter(child => child !== null) as ResultNode[] - return filterDown(filtered, words.slice(1)) + return filterDown(filtered, search.slice(1)) } -function traverseChildren(graph: GF, node: ResultNode, depth: number, allChildren: Set) { +function traverseChildren(graph: DirectedGraphOfNotes, node: ResultNode, depth: number, allChildren: Set) { if (depth > 2) return const neighbours = graph.outboundNeighborEntries(node.value) @@ -74,30 +73,33 @@ function getParents(graph: Graph, node: string) { return parents; } - -export function searchIndex(index: Index, qs: string): ResultNode[] { +export function searchIndex(graph: DirectedGraphOfNotes, qs: string): ResultNode[] { if (qs.length < 3) return [] - const words = qs.split(">") + const treeWords = qs.split(">") .map(w => w.toLowerCase().trim()) - const anchor = words.length > 0 ? words[0] : "" + .map(it => parseQuery(it)) + const filtered: ResultNode[] = [] - const nodes = index.graph.filterNodes(n => n.toLowerCase().includes(anchor)) + const nodes = graph.filterNodes(n => matchQuery(n, treeWords[0])) const allChildren = new Set() for (const node of nodes) { - const attrs = index.graph.getNodeAttributes(node) + const attrs = graph.getNodeAttributes(node) const newNode = { value: node, children: [], attrs: attrs, - parents: getParents(index.graph, node) + parents: getParents(graph, node) } filtered.push(newNode) - traverseChildren(index.graph, newNode, 0, allChildren) + traverseChildren(graph, newNode, 0, allChildren) } - return filterDown(filtered, words.slice(1)).filter(f => !allChildren.has(f.value)) + return filterDown(filtered, treeWords.slice(1)).filter(f => !allChildren.has(f.value)) + .sort((a, b) => b.children.length - a.children.length) } + + diff --git a/src/tree-builder.ts b/src/tree-builder.ts index 2583921..d269aed 100644 --- a/src/tree-builder.ts +++ b/src/tree-builder.ts @@ -1,6 +1,6 @@ import Graph from "graphology"; import {Notice} from "obsidian"; -import {getAPI, DataviewAPI} from "obsidian-dataview"; +import {getAPI} from "obsidian-dataview"; import {parseMarkdown} from "./parser"; import {Token} from "markdown-it"; @@ -29,19 +29,40 @@ export type GraphAttributes = { name?: string; } +export type DirectedGraphOfNotes = Graph + export type Index = { - graph: Graph, + graph: DirectedGraphOfNotes } export type Location = { path: string, - line: number + position: { + start: { + line: number, + col: number + }, + end: { + line: number, + col: number + } + } } export type DvList = { link: { path: string }, text: string, line: number, + position: { + start: { + line: number, + col: number + }, + end: { + line: number, + col: number + } + } parent: number, children: DvList[], tags: string[], @@ -102,11 +123,58 @@ function extractUrlFromMarkdown(markdown: string) { return markdown.match(/\(([^)]+)\)/)?.[1] || extractHttpUrlFromMarkdown(markdown) } -function shouldNotRender(item: DvList) { - return !item.text.startsWith("[") - && !item.text.startsWith('!') - && !item.text.contains('#') - && !item.text.contains('http'); +// not interested in plain text +function shouldSkip(lst: DvList) { + return !lst.text.startsWith("[") + && !lst.text.startsWith('!') + && !lst.text.includes('#') + && !lst.text.includes('http'); +} + +export function indexSinglePage(page: DvPage, graph: Graph) { + const pageRef = "[[" + page.file.name + "]]"; + addNode(graph, pageRef.toLowerCase(), { + fullMarkdownText: pageRef, + parsed: {page: page.file.name}, + location: { + path: page.file.path, + position: { + start: {line: 0, col: 0}, + end: {line: 0, col: 0} + } + }, + tokens: [] + }) + + for (const item of page.file.lists.values) { + if (shouldSkip(item)) continue + + addNode(graph, item.text.toLowerCase(), { + fullMarkdownText: item.text, + location: { + path: page.file.path, + position: item.position + }, + parsed: parseLine(item.text), + tokens: parseMarkdown(item.text, {}) + }) + + if (!item.parent) { + addEdge(graph, pageRef.toLowerCase(), item.text.toLowerCase(), {mtime: page.file.mtime.ts}) + } + + for (const child of item.children) { + if (shouldSkip(child)) continue + + addNode(graph, child.text.toLowerCase(), { + fullMarkdownText: child.text, + location: {path: page.file.path, position: child.position,}, + parsed: parseLine(child.text), + tokens: parseMarkdown(item.text, {}) + }) + addEdge(graph, item.text.toLowerCase(), child.text.toLowerCase(), {mtime: page.file.mtime.ts}) + } + } } export function indexTree(): Index | undefined { @@ -118,56 +186,17 @@ export function indexTree(): Index | undefined { } const pages = dv.pages("") - // .where(p => p.file.name == "My Teams") + // .where(p => p.file.name == "ImportantProjects") const graph = new Graph(); const idx: Index = {graph: graph} for (const dvp of pages) { - const page = dvp as DvPage - const pageRef = "[[" + page.file.name + "]]"; - addNode(graph, pageRef.toLowerCase(), { - fullMarkdownText: pageRef, - parsed: { page: page.file.name }, - location: { - path: page.file.path, - line: 0 - }, - tokens: [] - }) - - for (const item of page.file.lists.values) { - if (shouldNotRender(item)) continue - - addNode(graph, item.text.toLowerCase(), { - fullMarkdownText: item.text, - location: { - path: page.file.path, - line: item.line - }, - parsed: parseLine(item.text), - tokens: parseMarkdown(item.text, {}) - }) - - addEdge(graph, pageRef.toLowerCase(), item.text.toLowerCase(), {mtime: page.file.mtime.ts}) - - for (const child of item.children) { - if (shouldNotRender(child)) continue - - addNode(graph, child.text.toLowerCase(), { - fullMarkdownText: child.text, - location: { path: page.file.path, line: child.line,}, - parsed: parseLine(child.text), - tokens: parseMarkdown(item.text, {}) - }) - addEdge(graph, item.text.toLowerCase(), child.text.toLowerCase(), {mtime: page.file.mtime.ts}) - } - } + const page = dvp as DvPage; + indexSinglePage(page, graph); } - console.log(idx) - - return idx + return idx; } diff --git a/src/view/search.tsx b/src/view/search.tsx index fedd7ef..7b8c0b3 100644 --- a/src/view/search.tsx +++ b/src/view/search.tsx @@ -6,8 +6,8 @@ import {ResultNode, searchIndex} from "../search"; import Markdown from "react-markdown"; -export const SearchView = ({index}: { index: Index }) => { - const [idx, setIdx] = useState(index) +export const SearchView = () => { + const [idx, setIdx] = useState() const [search, setSearch] = useState("") const [results, setResults] = useState([]) const [pages, setPages] = useState(0) @@ -18,7 +18,16 @@ export const SearchView = ({index}: { index: Index }) => { } useEffect(() => { - setResults(searchIndex(idx, search)) + if (!idx) { + // console.log("Indexing") + refreshIndex() + } + }, [idx]); + + useEffect(() => { + if (!idx) return + + setResults(searchIndex(idx.graph, search)) setPages(0) }, [search, idx]) @@ -45,7 +54,7 @@ export const SearchPage = (props: {results: ResultNode[], page: number}) => { export const SearchTreeList = (props: { node: ResultNode, level: number }) => { - return
+ return
@@ -59,7 +68,7 @@ export const NodeView = (props: { node: ResultNode, index: number }) => { async function openFile(attrs: NodeAttributes) { if (app == undefined) return - await openFileAndHighlightLine(app, attrs.location.path, attrs.location.line) + await openFileAndHighlightLine(app, attrs.location.path, attrs.location.position.start, attrs.location.position.end) } const attrs = props.node.attrs @@ -75,7 +84,7 @@ export const NodeView = (props: { node: ResultNode, index: number }) => { className="search-result-file-match better-search-views-file-match markdown-preview-view markdown-rendered" onClick={() => openFile(attrs)} > - {`${props.index + 1}. ${text}`} + {props.index == 0 ? `**${text}**` : `${props.index}. ${text}`}
} diff --git a/src/view/treesearch.tsx b/src/view/treesearch.tsx index 1f0b0b1..d471679 100644 --- a/src/view/treesearch.tsx +++ b/src/view/treesearch.tsx @@ -26,13 +26,12 @@ export class TreeSearch extends ItemView { } async onOpen() { - const idx = indexTree() this.root = createRoot(this.containerEl.children[1]); this.root.render( - {idx != undefined ? :

Index not found

} +
, ); diff --git a/styles.css b/styles.css index 71cc60f..2553905 100644 --- a/styles.css +++ b/styles.css @@ -6,3 +6,7 @@ available in the app when your plugin is enabled. If your plugin does not need CSS, delete this file. */ + +.search-tree-container { + border-top: 1px solid red; +} diff --git a/tests/__mocks__/obsidian.ts b/tests/__mocks__/obsidian.ts new file mode 100644 index 0000000..08486ba --- /dev/null +++ b/tests/__mocks__/obsidian.ts @@ -0,0 +1,20 @@ +export class App { + vault: Vault; +} + +export class TFile { + path: string; + basename: string; + extension: string; + stat: { ctime: number; mtime: number; size: number }; + vault: Vault; +} + +export class Vault { + files: { [key: string]: TFile } = {}; + fileMap: { [key: string]: TFile } = {}; +} + +export class Notice { + constructor(message: string) {} +} diff --git a/tests/__mocks__/vaultFixture.ts b/tests/__mocks__/vaultFixture.ts new file mode 100644 index 0000000..bf57739 --- /dev/null +++ b/tests/__mocks__/vaultFixture.ts @@ -0,0 +1,1276 @@ +export const VAULT_PAGE = { + "file": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "folder": "0. Inbox/2024/09", + "name": "ImportantProjects", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "outlinks": { + "values": [ + { + "path": "0. Inbox/2024/09/2024-Sep-W37.md", + "display": "2024-Sep-W37", + "embed": false, + "type": "file" + }, + { + "path": "Project1", + "display": "Project1", + "embed": false, + "type": "file" + }, + { + "path": "Project1", + "display": "Project1", + "embed": false, + "type": "file" + }, + { + "path": "Project1 Task", + "display": "Project1 Task", + "embed": false, + "type": "file" + }, + { + "path": "Project2", + "display": "Project2", + "embed": false, + "type": "file" + }, + { + "path": "Project1", + "display": "Project1", + "embed": false, + "type": "file" + }, + { + "path": "Project2", + "display": "Project2", + "embed": false, + "type": "file" + } + ], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 7 + }, + "inlinks": { + "values": [], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 0 + }, + "etags": { + "values": [ + "#bookmark", + "#bookmark/important" + ], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 2 + }, + "tags": { + "values": [ + "#bookmark", + "#bookmark/important" + ], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 2 + }, + "aliases": { + "values": [], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 0 + }, + "lists": { + "values": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project1]]", + "tags": [], + "line": 3, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project1", + "display": "Project1", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[An external link to remember](https://company-reference) #bookmark", + "tags": [ + "#bookmark" + ], + "line": 4, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 4, + "col": 2, + "offset": 57 + }, + "end": { + "line": 4, + "col": 74, + "offset": 129 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 3 + } + ], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 3, + "col": 0, + "offset": 38 + }, + "end": { + "line": 3, + "col": 16, + "offset": 54 + } + }, + "subtasks": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[An external link to remember](https://company-reference) #bookmark", + "tags": [ + "#bookmark" + ], + "line": 4, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 4, + "col": 2, + "offset": 57 + }, + "end": { + "line": 4, + "col": 74, + "offset": 129 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 3 + } + ], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + } + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[An external link to remember](https://company-reference) #bookmark", + "tags": [ + "#bookmark" + ], + "line": 4, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 4, + "col": 2, + "offset": 57 + }, + "end": { + "line": 4, + "col": 74, + "offset": 129 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 3 + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project1]]", + "tags": [], + "line": 5, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project1", + "display": "Project1", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project1 Task]] with some optional inline context", + "tags": [], + "line": 6, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project1 Task", + "display": "Project1 Task", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + } + ], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 6, + "col": 2, + "offset": 149 + }, + "end": { + "line": 6, + "col": 64, + "offset": 211 + } + }, + "subtasks": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + } + ], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 5 + } + ], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 5, + "col": 0, + "offset": 130 + }, + "end": { + "line": 5, + "col": 16, + "offset": 146 + } + }, + "subtasks": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project1 Task]] with some optional inline context", + "tags": [], + "line": 6, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project1 Task", + "display": "Project1 Task", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + } + ], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 6, + "col": 2, + "offset": 149 + }, + "end": { + "line": 6, + "col": 64, + "offset": 211 + } + }, + "subtasks": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + } + ], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 5 + } + ], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + } + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project1 Task]] with some optional inline context", + "tags": [], + "line": 6, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project1 Task", + "display": "Project1 Task", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + } + ], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 6, + "col": 2, + "offset": 149 + }, + "end": { + "line": 6, + "col": 64, + "offset": 211 + } + }, + "subtasks": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + } + ], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 5 + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://task-tracker/item - this is something I should keep track of temporarily", + "tags": [], + "line": 7, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 7, + "col": 6, + "offset": 218 + }, + "end": { + "line": 7, + "col": 94, + "offset": 306 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 6 + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project2]]", + "tags": [], + "line": 8, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project2", + "display": "Project2", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://my-project-himepage #bookmark/important", + "tags": [ + "#bookmark/important" + ], + "line": 9, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 9, + "col": 2, + "offset": 326 + }, + "end": { + "line": 9, + "col": 53, + "offset": 377 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 8 + } + ], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 8, + "col": 0, + "offset": 307 + }, + "end": { + "line": 8, + "col": 16, + "offset": 323 + } + }, + "subtasks": [ + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://my-project-himepage #bookmark/important", + "tags": [ + "#bookmark/important" + ], + "line": 9, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 9, + "col": 2, + "offset": 326 + }, + "end": { + "line": 9, + "col": 53, + "offset": 377 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 8 + } + ], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + } + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "https://my-project-himepage #bookmark/important", + "tags": [ + "#bookmark/important" + ], + "line": 9, + "lineCount": 1, + "list": 3, + "outlinks": [], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 9, + "col": 2, + "offset": 326 + }, + "end": { + "line": 9, + "col": 53, + "offset": 377 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "parent": 8 + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project1]]", + "tags": [], + "line": 10, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project1", + "display": "Project1", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 10, + "col": 0, + "offset": 378 + }, + "end": { + "line": 10, + "col": 16, + "offset": 394 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + } + }, + { + "symbol": "-", + "link": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "section": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + }, + "text": "[[Project2]]", + "tags": [], + "line": 11, + "lineCount": 1, + "list": 3, + "outlinks": [ + { + "path": "Project2", + "display": "Project2", + "embed": false, + "type": "file" + } + ], + "path": "0. Inbox/2024/09/ImportantProjects.md", + "children": [], + "task": false, + "annotated": false, + "position": { + "start": { + "line": 11, + "col": 0, + "offset": 395 + }, + "end": { + "line": 11, + "col": 14, + "offset": 409 + } + }, + "subtasks": [], + "real": false, + "header": { + "path": "0. Inbox/2024/09/ImportantProjects.md", + "embed": false, + "type": "file" + } + } + ], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 9 + }, + "tasks": { + "values": [], + "settings": { + "renderNullAs": "\\-", + "taskCompletionTracking": false, + "taskCompletionUseEmojiShorthand": false, + "taskCompletionText": "completion", + "taskCompletionDateFormat": "yyyy-MM-dd", + "recursiveSubTaskCompletion": false, + "warnOnEmptyResult": true, + "refreshEnabled": true, + "refreshInterval": 2500, + "defaultDateFormat": "MMMM dd, yyyy", + "defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy", + "maxRecursiveRenderDepth": 4, + "tableIdColumnName": "File", + "tableGroupColumnName": "Group", + "showResultCount": true, + "allowHtml": true, + "inlineQueryPrefix": "=", + "inlineJsQueryPrefix": "$=", + "inlineQueriesInCodeblocks": true, + "enableInlineDataview": true, + "enableDataviewJs": true, + "enableInlineDataviewJs": true, + "prettyRenderInlineFields": true, + "prettyRenderInlineFieldsInLivePreview": true, + "dataviewJsKeyword": "dataviewjs" + }, + "length": 0 + }, + "ctime": "2024-09-14T10:24:30.160+03:00", + "cday": "2024-09-14T00:00:00.000+03:00", + "mtime": "2024-09-15T15:59:05.483+03:00", + "mday": "2024-09-15T00:00:00.000+03:00", + "size": 411, + "starred": false, + "frontmatter": { + "related": [ + "[[2024-Sep-W37]]" + ] + }, + "ext": "md" +}, + "related": [ + { + "path": "0. Inbox/2024/09/2024-Sep-W37.md", + "embed": false, + "type": "file" + } +] +} diff --git a/tests/fixture.test.ts b/tests/fixture.test.ts new file mode 100644 index 0000000..1e20db1 --- /dev/null +++ b/tests/fixture.test.ts @@ -0,0 +1,16 @@ +import {createFixture} from "./fixtures"; +import {describe, expect, test} from "@jest/globals"; + +describe('createFixture', () => { + test("createFixture", () => { + const fixture = createFixture("test", ` +- [[Test]] + - [[TestChild]] + - [[TestGrandChild]] + `) + + expect(fixture.file.lists.values.length).toBe(3) + expect(fixture.file.lists.values[0].children.length).toBe(1) + expect(fixture.file.lists.values[0].children[0].children.length).toBe(1) + }) +}); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..9889742 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,117 @@ +import {DvList, DvPage, EdgeAttributes, GraphAttributes, indexSinglePage, NodeAttributes} from "../src/tree-builder"; +import Graph from "graphology"; +import {ResultNode, searchIndex} from "../src/search"; +import {expect} from "@jest/globals"; + +export function buildIndexFromFixture(page: string, lines: string) { + const graph = new Graph() + const pageFixture = createFixture(page, lines); + indexSinglePage(pageFixture, graph); + return graph; +} + +export function renderTextResult(result: ResultNode[]): string { + return renderResult(result).join("\n") +} + +export function renderResult(result: ResultNode[], indent = ""): string[] { + if (result.length === 0) { + return [] + } + + return result.flatMap(it => [indent + it.attrs.tokens.map(t => t.content).join(" "), + ...renderResult(it.children, indent + " ")]) +} + +export function testGraphSearch(graph: Graph, qs: string, expected: string) { + const resultNodes = searchIndex(graph, qs); + const result = renderTextResult(resultNodes); + const exp = expected.trim(); + expect(result).toEqual(exp) +} + +export function createFixture(name: string, lines: string) { + return createPageFixture(name, lines.trim().split("\n")) +} + +function createPageFixture(path: string, lines: string[]): DvPage { + const name = path.replace(".md", ""); + const page = { + "aliases": [], + "file": { + "name": name, + "path": path, + "lists": { + "values": lines.map((line, i) => createItemFixture(name, line, i)) + }, + "frontmatter": {}, + "mtime": { + "ts": 0, + "c": { + "year": 0, + "month": 0, + "day": 0 + } + }, + }, + tags: [] + } + + page.file.lists.values = embedChildren(page.file.lists.values) + + return page +} + +// do what dataview does: all elements that point to a parent line should be children of that line +export function embedChildren(lines: DvList[]): DvList[] { + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (line.parent !== 0) { + lines[line.parent - 1].children.push(line) + } + } + return lines +} + +function createItemFixture(file: string, line: string, lineNum: number): DvList { + return { + "link": { + "path": file, + // "embed": false, + // "type": "file" + }, + // "section": { + // "path": file, + // "embed": false, + // "type": "file" + // }, + "text": line.replace("-", "").trim(), + "tags": [], + "line": lineNum, + // "lineCount": 1, + // "list": 4, + // "path": file, + "children": [], + // "task": false, + // "annotated": false, + "parent": line.startsWith("-") ? 0 : lineNum, + "position": { + "start": { + "line": lineNum, + "col": 0, + // "offset": 45 + }, + "end": { + "line": lineNum, + "col": 25, + // "offset": 70 + } + }, + // "real": false, + // "header": { + // "path": lineNum, + // "embed": false, + // "type": "file" + // } + } +} diff --git a/tests/query.test.ts b/tests/query.test.ts new file mode 100644 index 0000000..46e5a3b --- /dev/null +++ b/tests/query.test.ts @@ -0,0 +1,21 @@ +import {describe, expect, it} from "@jest/globals"; +import {matchQuery, parseQuery} from "../src/query"; + +describe('parseSearchQuery', () => { + it('queries', () => { + const text = "This is a sample text for testing"; + + function matchQueryT(text: string, query: string) { + return matchQuery(text, parseQuery(query)); + } + + expect(matchQueryT(text, "-for")).toBeFalsy(); + expect(matchQueryT(text, "example")).toBeFalsy(); + expect(matchQueryT(text, "for")).toBeTruthy(); + expect(matchQueryT(text, "-example")).toBeTruthy(); + expect(matchQueryT(text, "sample AND -example")).toBeTruthy(); + expect(matchQueryT(text, "sample text OR testing -example")).toBeTruthy(); + expect(matchQueryT(text, "sample -text OR testing -for")).toBeFalsy(); + expect(matchQueryT(text, "-")).toBeFalsy(); + }); +}); diff --git a/tests/search.test.ts b/tests/search.test.ts new file mode 100644 index 0000000..bda75ec --- /dev/null +++ b/tests/search.test.ts @@ -0,0 +1,56 @@ +import {DvPage, EdgeAttributes, GraphAttributes, indexSinglePage, NodeAttributes} from '../src/tree-builder'; +import {describe, expect, it, jest} from "@jest/globals"; +import {searchIndex} from "../src/search"; +import {VAULT_PAGE} from './__mocks__/vaultFixture'; +import Graph from "graphology"; + +// Mock the Obsidian API +jest.mock('obsidian', () => ({ + App: jest.fn(), + TFile: jest.fn(), + Vault: jest.fn(), + Notice: jest.fn(), +})); + +jest.mock('obsidian-dataview', () => ({ + getAPI: jest.fn(), +})); + +function buildIndex() { + const graph = new Graph() + indexSinglePage(VAULT_PAGE as unknown as DvPage, graph); + return graph; +} + +describe('indexTree', () => { + it('should index the sample vault correctly', () => { + const graph = buildIndex(); + + expect(graph.nodes()).toContain('[[importantprojects]]'); //lowercase keys + expect(graph.nodes()).toContain('[[project1]]'); + }); + + it('should search the sample vault correctly', () => { + const graph = buildIndex(); + + const result = searchIndex(graph, 'Important'); + expect(result.length).toBe(1) // importantprojects + expect(result[0].children.length).toBe(2) // project1 and project2 + }) +}); + +describe('search operators', () => { + it("should return a nested search filter", () => { + const graph = buildIndex(); + + const result = searchIndex(graph, 'important > project2'); + expect(result[0].children[0].value).toBe('[[project2]]') + }) + + it("should do an AND when multiple search terms", () => { + const graph = buildIndex(); + + const result = searchIndex(graph, 'task tracker'); + expect(result.length).toBe(1) + }) +})