├── .vscode └── extensions.json ├── src ├── vite-env.d.ts ├── lib │ ├── dataviewParser.ts │ ├── Counter.svelte │ ├── templateLoader.ts │ ├── MapViewLazy.svelte │ ├── MapView.svelte │ ├── Toolbox.svelte │ ├── About.svelte │ ├── DATE_FUNCTIONS.md │ ├── BoardView.svelte │ ├── GalleryView.svelte │ ├── mockDataGeneratorLazy.ts │ ├── templateExamples.ts │ ├── CalendarView.svelte │ ├── mockDataGenerator.ts │ ├── BasesUpdater.svelte │ ├── basesUpdater.ts │ └── DataviewConverter.svelte ├── dataview-parser │ ├── readme.md │ ├── index.ts │ ├── source-types.ts │ ├── field.ts │ ├── normalize.ts │ ├── query-types.ts │ ├── query-parse.ts │ └── expression-parse.ts ├── main.ts ├── App.svelte ├── app.css └── assets │ └── svelte.svg ├── tsconfig.json ├── svelte.config.js ├── .gitignore ├── index.html ├── public └── vite.svg ├── tsconfig.app.json ├── tsconfig.node.json ├── package.json ├── vite.config.ts ├── README.md └── docs └── functions.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/lib/dataviewParser.ts: -------------------------------------------------------------------------------- 1 | // Re-export the main parsing function from the new parser 2 | export { parseDataviewTable } from "../dataview-parser/index"; 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/dataview-parser/readme.md: -------------------------------------------------------------------------------- 1 | Code for parsing dataview queries. 2 | 3 | Original code from [Obsidian Dataview](https://github.com/blacksmithgu/obsidian-dataview). 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte' 2 | import './app.css' 3 | import App from './App.svelte' 4 | 5 | const app = mount(App, { 6 | target: document.getElementById('app')!, 7 | }) 8 | 9 | export default app 10 | -------------------------------------------------------------------------------- /src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bases Toolbox 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force" 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/templateLoader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy loader for template examples to reduce initial bundle size 3 | */ 4 | 5 | let templatesCache: any = null; 6 | 7 | export async function loadTemplates() { 8 | if (templatesCache) { 9 | return templatesCache; 10 | } 11 | 12 | try { 13 | const { baseTemplates } = await import('./templateExamples'); 14 | templatesCache = baseTemplates; 15 | return baseTemplates; 16 | } catch (error) { 17 | console.error('Failed to load templates:', error); 18 | return {}; 19 | } 20 | } 21 | 22 | export function getTemplateNames(): string[] { 23 | if (!templatesCache) { 24 | return []; 25 | } 26 | return Object.keys(templatesCache); 27 | } 28 | 29 | export function hasTemplatesLoaded(): boolean { 30 | return templatesCache !== null; 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bases-online-preview", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" 11 | }, 12 | "dependencies": { 13 | "@faker-js/faker": "^9.8.0", 14 | "@types/js-yaml": "^4.0.9", 15 | "@types/leaflet": "^1.9.18", 16 | "@types/luxon": "^3.6.2", 17 | "@types/parsimmon": "^1.10.9", 18 | "emoji-regex": "^10.4.0", 19 | "js-yaml": "^4.1.0", 20 | "leaflet": "^1.9.4", 21 | "luxon": "^3.6.1", 22 | "parsimmon": "^1.18.1" 23 | }, 24 | "devDependencies": { 25 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 26 | "@tsconfig/svelte": "^5.0.4", 27 | "svelte": "^5.28.1", 28 | "svelte-check": "^4.1.6", 29 | "typescript": "~5.8.3", 30 | "vite": "^6.3.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 9 | 10 |
11 |

All rights @boninall reserved

12 |
13 |
14 | 15 | 43 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()], 7 | build: { 8 | rollupOptions: { 9 | output: { 10 | manualChunks: { 11 | // Vendor libraries 12 | 'vendor-core': ['svelte'], 13 | 'vendor-utils': ['js-yaml', 'luxon', 'parsimmon', 'emoji-regex'], 14 | 'vendor-faker': ['@faker-js/faker'], 15 | 'vendor-leaflet': ['leaflet'], 16 | 17 | // Application chunks 18 | 'dataview-parser': [ 19 | './src/dataview-parser/index.ts', 20 | './src/dataview-parser/query-parse.ts', 21 | './src/dataview-parser/transformer.ts', 22 | './src/dataview-parser/expression-parse.ts', 23 | './src/dataview-parser/normalize.ts' 24 | ], 25 | } 26 | } 27 | }, 28 | // Increase chunk size warning limit to 3000kb since faker.js is intentionally large and lazy-loaded 29 | chunkSizeWarningLimit: 3000 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Bases Toolbox 2 | 3 | A comprehensive toolbox for working with Obsidian Bases files, featuring: 4 | 5 | 1. **Bases Preview** - Preview and test your Obsidian Bases files 6 | 2. **Dataview Converter** - Convert Dataview TABLE queries to Bases format 7 | 8 | ## Features 9 | 10 | ### Bases Preview 11 | 12 | - Preview how your .base file will look in Obsidian 13 | - Test different view types: Table, Board, Gallery, Map, Calendar 14 | - Drag and drop a .base file or edit YAML directly 15 | - Try pre-built templates to get started quickly 16 | - Generate random mock data to test your views 17 | 18 | ### Dataview Converter 19 | 20 | - Convert existing Dataview TABLE queries to Bases YAML 21 | - Supports most common Dataview features: 22 | - Fields with aliases 23 | - FROM source 24 | - WHERE conditions (AND, OR) 25 | - SORT with ASC/DESC directions 26 | - LIMIT and GROUP BY 27 | - Copy converted YAML to create your .base files 28 | 29 | ## Usage 30 | 31 | 1. Clone this repository 32 | 2. Install dependencies with `npm install` or `pnpm install` 33 | 3. Run the development server with `npm run dev` or `pnpm dev` 34 | 4. Open your browser to the localhost URL displayed in the terminal 35 | 36 | ## Development 37 | 38 | This application is built with: 39 | 40 | - Svelte 41 | - TypeScript 42 | - Vite 43 | 44 | ## Building 45 | 46 | To build for production: 47 | 48 | ```bash 49 | npm run build 50 | # or 51 | pnpm build 52 | ``` 53 | 54 | The build files will be in the `dist` folder. 55 | 56 | ## License 57 | 58 | MIT -------------------------------------------------------------------------------- /src/dataview-parser/index.ts: -------------------------------------------------------------------------------- 1 | import { parseQuery } from "./query-parse"; 2 | import { DataviewToBasesTransformer } from "./transformer"; 3 | 4 | /** 5 | * Parse a Dataview TABLE query and convert it to Bases YAML 6 | * @param dataviewQuery The Dataview query string 7 | * @param placeFiltersInView Whether to place filters in the view (true) or globally (false) 8 | * @returns The Bases YAML string 9 | */ 10 | export function parseDataviewTable( 11 | dataviewQuery: string, 12 | placeFiltersInView: boolean = true 13 | ): string { 14 | try { 15 | // Parse the query using Parsimmon parser 16 | const parseResult = parseQuery(dataviewQuery); 17 | 18 | console.log(parseResult); 19 | 20 | if (!parseResult.successful) { 21 | throw new Error(`Failed to parse Dataview query: ${parseResult.error}`); 22 | } 23 | 24 | // Transform the AST to Bases structure 25 | const transformer = new DataviewToBasesTransformer(); 26 | return transformer.toYaml(parseResult.value, placeFiltersInView); 27 | } catch (error) { 28 | console.error("Error parsing Dataview query:", error); 29 | if (error instanceof Error) { 30 | return `# Error parsing Dataview query: ${error.message}`; 31 | } 32 | return "# Error parsing Dataview query"; 33 | } 34 | } 35 | 36 | // Re-export types and utilities for external use 37 | export { parseQuery } from "./query-parse"; 38 | export { DataviewToBasesTransformer } from "./transformer"; 39 | export type { Query, QueryHeader, QueryOperation } from "./query-types"; 40 | export type { Field } from "./field"; 41 | export type { Source } from "./source-types"; 42 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 3 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 4 | line-height: 1.5; 5 | font-weight: 400; 6 | color: #333; 7 | background-color: #f5f6f8; 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | html, 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | height: 100%; 19 | overflow: hidden; /* 防止滚动 */ 20 | } 21 | 22 | body { 23 | position: fixed; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | #app { 29 | width: 100%; 30 | height: 100vh; 31 | margin: 0; 32 | padding: 0; 33 | overflow: hidden; 34 | } 35 | 36 | /* For code/yaml sections */ 37 | pre, 38 | code { 39 | font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", 40 | monospace; 41 | } 42 | 43 | /* Reset some default button styles */ 44 | button { 45 | border: none; 46 | background: none; 47 | font-family: inherit; 48 | font-size: inherit; 49 | cursor: pointer; 50 | padding: 0; 51 | } 52 | 53 | /* Let our components define their own button styles */ 54 | button:focus { 55 | outline: none; 56 | } 57 | 58 | /* Add small transition to buttons */ 59 | button { 60 | transition: background-color 0.2s, color 0.2s, border-color 0.2s; 61 | } 62 | 63 | /* Basic link styles */ 64 | a { 65 | color: #5865f2; 66 | text-decoration: none; 67 | } 68 | 69 | a:hover { 70 | text-decoration: underline; 71 | } 72 | 73 | /* Scrollbar styling */ 74 | ::-webkit-scrollbar { 75 | width: 8px; 76 | height: 8px; 77 | } 78 | 79 | ::-webkit-scrollbar-track { 80 | background: #f1f1f1; 81 | } 82 | 83 | ::-webkit-scrollbar-thumb { 84 | background: #c1c1c1; 85 | border-radius: 4px; 86 | } 87 | 88 | ::-webkit-scrollbar-thumb:hover { 89 | background: #a1a1a1; 90 | } 91 | -------------------------------------------------------------------------------- /src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/MapViewLazy.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | {#if loading} 28 |
29 |
30 |

Loading map component...

31 |
32 | {:else if error} 33 |
34 |

Error: {error}

35 |
36 | {:else if MapViewComponent} 37 | 44 | {/if} 45 | 46 | 82 | -------------------------------------------------------------------------------- /src/lib/MapView.svelte: -------------------------------------------------------------------------------- 1 | 96 | 97 | 126 | 127 |
-------------------------------------------------------------------------------- /src/dataview-parser/source-types.ts: -------------------------------------------------------------------------------- 1 | /** AST implementation for queries over data sources. */ 2 | 3 | /** The source of files for a query. */ 4 | export type Source = 5 | | TagSource 6 | | CsvSource 7 | | FolderSource 8 | | LinkSource 9 | | EmptySource 10 | | NegatedSource 11 | | BinaryOpSource; 12 | /** Valid operations for combining sources. */ 13 | export type SourceOp = "&" | "|"; 14 | 15 | /** A tag as a source of data. */ 16 | export interface TagSource { 17 | type: "tag"; 18 | /** The tag to source from. */ 19 | tag: string; 20 | } 21 | 22 | /** A csv as a source of data. */ 23 | export interface CsvSource { 24 | type: "csv"; 25 | /** The path to the CSV file. */ 26 | path: string; 27 | } 28 | 29 | /** A folder prefix as a source of data. */ 30 | export interface FolderSource { 31 | type: "folder"; 32 | /** The folder prefix to source from. */ 33 | folder: string; 34 | } 35 | 36 | /** Either incoming or outgoing links to a given file. */ 37 | export interface LinkSource { 38 | type: "link"; 39 | /** The file to look for links to/from. */ 40 | file: string; 41 | /** 42 | * The direction to look - if incoming, then all files linking to the target file. If outgoing, then all files 43 | * which the file links to. 44 | */ 45 | direction: "incoming" | "outgoing"; 46 | } 47 | 48 | /** A source which is everything EXCEPT the files returned by the given source. */ 49 | export interface NegatedSource { 50 | type: "negate"; 51 | /** The source to negate. */ 52 | child: Source; 53 | } 54 | 55 | /** A source which yields nothing. */ 56 | export interface EmptySource { 57 | type: "empty"; 58 | } 59 | 60 | /** A source made by combining subsources with a logical operators. */ 61 | export interface BinaryOpSource { 62 | type: "binaryop"; 63 | op: SourceOp; 64 | left: Source; 65 | right: Source; 66 | } 67 | 68 | /** Utility functions for creating and manipulating sources. */ 69 | export namespace Sources { 70 | /** Create a source which searches from a tag. */ 71 | export function tag(tag: string): TagSource { 72 | return { type: "tag", tag }; 73 | } 74 | 75 | /** Create a source which fetches from a CSV file. */ 76 | export function csv(path: string): CsvSource { 77 | return { type: "csv", path }; 78 | } 79 | 80 | /** Create a source which searches for files under a folder prefix. */ 81 | export function folder(prefix: string): FolderSource { 82 | return { type: "folder", folder: prefix }; 83 | } 84 | 85 | /** Create a source which searches for files which link to/from a given file. */ 86 | export function link(file: string, incoming: boolean): LinkSource { 87 | return { 88 | type: "link", 89 | file, 90 | direction: incoming ? "incoming" : "outgoing", 91 | }; 92 | } 93 | 94 | /** Create a source which joins two sources by a logical operator (and/or). */ 95 | export function binaryOp(left: Source, op: SourceOp, right: Source): Source { 96 | return { type: "binaryop", left, op, right }; 97 | } 98 | 99 | /** Create a source which takes the intersection of two sources. */ 100 | export function and(left: Source, right: Source): Source { 101 | return { type: "binaryop", left, op: "&", right }; 102 | } 103 | 104 | /** Create a source which takes the union of two sources. */ 105 | export function or(left: Source, right: Source): Source { 106 | return { type: "binaryop", left, op: "|", right }; 107 | } 108 | 109 | /** Create a source which negates the underlying source. */ 110 | export function negate(child: Source): NegatedSource { 111 | return { type: "negate", child }; 112 | } 113 | 114 | export function empty(): EmptySource { 115 | return { type: "empty" }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/Toolbox.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 |
41 |
42 | 48 | 54 | 60 | 66 |
67 |
68 | 73 |
74 |
75 | 76 |
77 | {#if activeTab === TABS.BASES_PREVIEW} 78 | 79 | {:else if activeTab === TABS.DATAVIEW_CONVERTER} 80 | 81 | {:else if activeTab === TABS.BASES_UPDATER} 82 | 83 | {:else if activeTab === TABS.ABOUT} 84 | 85 | {/if} 86 |
87 |
88 | 89 | -------------------------------------------------------------------------------- /src/dataview-parser/field.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Duration } from "luxon"; 2 | import type { Link } from "./normalize"; 3 | /** Defines the AST for a field which can be evaluated. */ 4 | export type Literal = 5 | | boolean 6 | | number 7 | | string 8 | | Array 9 | | Function 10 | | null 11 | | HTMLElement 12 | | DateTime 13 | | Duration 14 | | Link; 15 | 16 | /** Comparison operators which yield true/false. */ 17 | export type CompareOp = ">" | ">=" | "<=" | "<" | "=" | "!="; 18 | /** Arithmetic operators which yield numbers and other values. */ 19 | export type ArithmeticOp = "+" | "-" | "*" | "/" | "%" | "&" | "|"; 20 | /** All valid binary operators. */ 21 | export type BinaryOp = CompareOp | ArithmeticOp; 22 | /** A (potentially computed) field to select or compare against. */ 23 | export type Field = 24 | | BinaryOpField 25 | | VariableField 26 | | LiteralField 27 | | FunctionField 28 | | IndexField 29 | | NegatedField 30 | | LambdaField 31 | | ObjectField 32 | | ListField; 33 | 34 | /** Literal representation of some field type. */ 35 | export interface LiteralField { 36 | type: "literal"; 37 | value: Literal; 38 | } 39 | 40 | /** A variable field for a variable with a given name. */ 41 | export interface VariableField { 42 | type: "variable"; 43 | name: string; 44 | } 45 | 46 | /** A list, which is an ordered collection of fields. */ 47 | export interface ListField { 48 | type: "list"; 49 | values: Field[]; 50 | } 51 | 52 | /** An object, which is a mapping of name to field. */ 53 | export interface ObjectField { 54 | type: "object"; 55 | values: Record; 56 | } 57 | 58 | /** A binary operator field which combines two subnodes somehow. */ 59 | export interface BinaryOpField { 60 | type: "binaryop"; 61 | left: Field; 62 | right: Field; 63 | op: BinaryOp; 64 | } 65 | 66 | /** A function field which calls a function on 0 or more arguments. */ 67 | export interface FunctionField { 68 | type: "function"; 69 | /** Either the name of the function being called, or a Function object. */ 70 | func: Field; 71 | /** The arguments being passed to the function. */ 72 | arguments: Field[]; 73 | } 74 | 75 | export interface LambdaField { 76 | type: "lambda"; 77 | /** An ordered list of named arguments. */ 78 | arguments: string[]; 79 | /** The field which should be evaluated with the arguments in context. */ 80 | value: Field; 81 | } 82 | 83 | /** A field which indexes a variable into another variable. */ 84 | export interface IndexField { 85 | type: "index"; 86 | /** The field to index into. */ 87 | object: Field; 88 | /** The index. */ 89 | index: Field; 90 | } 91 | 92 | /** A field which negates the value of the original field. */ 93 | export interface NegatedField { 94 | type: "negated"; 95 | /** The child field to negated. */ 96 | child: Field; 97 | } 98 | 99 | /** Utility methods for creating & comparing fields. */ 100 | export namespace Fields { 101 | export function variable(name: string): VariableField { 102 | return { type: "variable", name }; 103 | } 104 | 105 | export function literal(value: Literal): LiteralField { 106 | return { type: "literal", value }; 107 | } 108 | 109 | export function binaryOp(left: Field, op: BinaryOp, right: Field): Field { 110 | return { type: "binaryop", left, op, right } as BinaryOpField; 111 | } 112 | 113 | export function index(obj: Field, index: Field): IndexField { 114 | return { type: "index", object: obj, index }; 115 | } 116 | 117 | /** Converts a string in dot-notation-format into a variable which indexes. */ 118 | export function indexVariable(name: string): Field { 119 | let parts = name.split("."); 120 | let result: Field = Fields.variable(parts[0]); 121 | for (let index = 1; index < parts.length; index++) { 122 | result = Fields.index(result, Fields.literal(parts[index])); 123 | } 124 | 125 | return result; 126 | } 127 | 128 | export function lambda(args: string[], value: Field): LambdaField { 129 | return { type: "lambda", arguments: args, value }; 130 | } 131 | 132 | export function func(func: Field, args: Field[]): FunctionField { 133 | return { type: "function", func, arguments: args }; 134 | } 135 | 136 | export function list(values: Field[]): ListField { 137 | return { type: "list", values }; 138 | } 139 | 140 | export function object(values: Record): ObjectField { 141 | return { type: "object", values }; 142 | } 143 | 144 | export function negate(child: Field): NegatedField { 145 | return { type: "negated", child }; 146 | } 147 | 148 | export function isCompareOp(op: BinaryOp): op is CompareOp { 149 | return ( 150 | op == "<=" || 151 | op == "<" || 152 | op == ">" || 153 | op == ">=" || 154 | op == "!=" || 155 | op == "=" 156 | ); 157 | } 158 | 159 | export const NULL = Fields.literal(null); 160 | } 161 | -------------------------------------------------------------------------------- /src/lib/About.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |

About Bases Toolbox

4 | 5 |

6 | Bases Toolbox is a comprehensive online preview tool designed to help you work with Obsidian's Bases feature more effectively. 7 | This project provides various utilities and preview capabilities to enhance your productivity when working with structured data in Obsidian. 8 |

9 | 10 |

Features

11 |
    12 |
  • Bases Preview: Visualize and interact with your Bases data in different views (Table, Calendar, Gallery, Board, Map)
  • 13 |
  • Dataview Converter: Convert Dataview queries to Bases format seamlessly
  • 14 |
  • Live Preview: See real-time updates as you modify your queries
  • 15 |
  • Multiple View Types: Support for various data visualization formats
  • 16 |
17 | 18 |

Learn More About Bases

19 |

20 | For detailed information about Obsidian's Bases feature and how to use it effectively, 21 | please visit the official documentation: 22 |

23 | 24 | 25 | 26 | 27 | 28 | 29 | Obsidian Bases Documentation 30 | 31 | 32 |

Open Source

33 |

34 | This project is open source and available on GitHub. Contributions, bug reports, and feature requests are welcome! 35 |

36 | 37 | 45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /src/lib/DATE_FUNCTIONS.md: -------------------------------------------------------------------------------- 1 | # Dataview to Bases Date Function Conversion 2 | 3 | This document explains how Dataview date expressions are converted to Bases date functions. 4 | 5 | ## Supported Date Conversions 6 | 7 | | Dataview Syntax | Bases Syntax | Description | 8 | | ----------------------------------------- | ------------------------------ | ------------------------------- | 9 | | `date(today)` | `now()` | Current date and time | 10 | | `date(yesterday)` | `dateModify(now(), "-1 day")` | Yesterday's date | 11 | | `date(tomorrow)` | `dateModify(now(), "1 day")` | Tomorrow's date | 12 | | `dur(N days)` | `"N days"` | Duration of N days | 13 | | `dur(N weeks)` | `"N weeks"` | Duration of N weeks | 14 | | `dur(N months)` | `"N months"` | Duration of N months | 15 | | `expr + dur(N units)` | `dateModify(expr, "N units")` | Add a duration to a date | 16 | | `expr - dur(N units)` | `dateModify(expr, "-N units")` | Subtract a duration from a date | 17 | | `now() + "N units"` or `expr + "N units"` | `dateModify(expr, "N units")` | Add a string duration to a date | 18 | | `now() - "N units"` or `expr - "N units"` | `dateModify(expr, "-N units")` | Subtract a string duration | 19 | 20 | ## Examples 21 | 22 | ### Example 1: Due date within the next week 23 | 24 | **Dataview:** 25 | 26 | ``` 27 | due >= date(today) AND due <= date(today) + dur(7 days) 28 | ``` 29 | 30 | **Bases:** 31 | 32 | ``` 33 | due >= now() AND due <= dateModify(now(), "7 days") 34 | ``` 35 | 36 | ### Example 2: Completed yesterday 37 | 38 | **Dataview:** 39 | 40 | ``` 41 | completion = date(yesterday) 42 | ``` 43 | 44 | **Bases:** 45 | 46 | ``` 47 | completion == dateModify(now(), "-1 day") 48 | ``` 49 | 50 | ### Example 3: Starting within the past two weeks 51 | 52 | **Dataview:** 53 | 54 | ``` 55 | start_date >= date(today) - dur(2 weeks) 56 | ``` 57 | 58 | **Bases:** 59 | 60 | ``` 61 | start_date >= dateModify(now(), "-2 weeks") 62 | ``` 63 | 64 | ### Example 4: Due tomorrow 65 | 66 | **Dataview:** 67 | 68 | ``` 69 | deadline < date(tomorrow) 70 | ``` 71 | 72 | **Bases:** 73 | 74 | ``` 75 | deadline < dateModify(now(), "1 day") 76 | ``` 77 | 78 | ### Example 5: Expression with date first 79 | 80 | **Dataview:** 81 | 82 | ``` 83 | (date(today) + dur(5 days)) > due_date 84 | ``` 85 | 86 | **Bases:** 87 | 88 | ``` 89 | dateModify(now(), "5 days") > due_date 90 | ``` 91 | 92 | ### Example 6: Field with already converted duration 93 | 94 | **Dataview (after partial conversion):** 95 | 96 | ``` 97 | due >= now() AND due <= now() + "7 days" 98 | ``` 99 | 100 | **Final Bases:** 101 | 102 | ``` 103 | due >= now() AND due <= dateModify(now(), "7 days") 104 | ``` 105 | 106 | ## Multi-Stage Processing Explained 107 | 108 | The date conversion happens in two stages: 109 | 110 | 1. First, Dataview date functions are converted: 111 | 112 | - `date(today)` becomes `now()` 113 | - `date(tomorrow)` becomes `dateModify(now(), "1 day")` 114 | - `date(yesterday)` becomes `dateModify(now(), "-1 day")` 115 | - `dur(7 days)` becomes `"7 days"` 116 | 117 | 2. Then, date math expressions are converted: 118 | - `now() + "7 days"` becomes `dateModify(now(), "7 days")` 119 | - `date_field - "2 weeks"` becomes `dateModify(date_field, "-2 weeks")` 120 | - `date(today) + dur(3 months)` becomes `dateModify(now(), "3 months")` 121 | 122 | This multi-stage process ensures all date calculations are properly converted to Bases format. 123 | 124 | ## Available Bases Date Functions 125 | 126 | All of these Bases date functions are supported: 127 | 128 | - `now()` - Returns the current date and time 129 | - `dateModify(datetime, duration)` - Modifies a date by a duration 130 | - `date_diff(datetime1, datetime2)` - Gets the difference between two dates in milliseconds 131 | - `dateEquals(datetime1, datetime2)` - Checks if two dates are equal 132 | - `dateNotEquals(datetime1, datetime2)` - Checks if two dates are not equal 133 | - `dateBefore(datetime1, datetime2)` - Checks if the first date is before the second 134 | - `dateAfter(datetime1, datetime2)` - Checks if the first date is after the second 135 | - `dateOnOrBefore(datetime1, datetime2)` - Checks if the first date is on or before the second 136 | - `dateOnOrAfter(datetime1, datetime2)` - Checks if the first date is on or after the second 137 | - `year(date)` - Gets the year from a date 138 | - `month(date)` - Gets the month from a date 139 | - `day(date)` - Gets the day from a date 140 | - `hour(datetime)` - Gets the hour from a date 141 | - `minute(datetime)` - Gets the minute from a date 142 | - `second(datetime)` - Gets the second from a date 143 | -------------------------------------------------------------------------------- /src/lib/BoardView.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
50 |
51 | {#each groupValues as groupValue} 52 |
53 |
54 |

{groupValue}

55 | {groupedFiles[groupValue].length} 56 |
57 |
58 | {#each groupedFiles[groupValue] as file} 59 |
60 |

{getPropertyValue(file, titleField)}

61 | {#if descriptionField} 62 |

{getPropertyValue(file, descriptionField)}

63 | {/if} 64 |
65 | {#if file.tags && file.tags.length} 66 |
67 | {#each file.tags.slice(0, 3) as tag} 68 | {tag} 69 | {/each} 70 |
71 | {/if} 72 | {#if file.priority} 73 |
74 | {file.priority} 75 |
76 | {/if} 77 |
78 |
79 | {/each} 80 |
81 |
82 | {/each} 83 |
84 |
85 | 86 | -------------------------------------------------------------------------------- /src/lib/GalleryView.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 74 | 75 | -------------------------------------------------------------------------------- /src/lib/mockDataGeneratorLazy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy-loaded mock data generator to reduce initial bundle size 3 | */ 4 | 5 | export interface MockFile { 6 | file: { 7 | file: string; 8 | name: string; 9 | ctime: Date; 10 | mtime: Date; 11 | ext: string; 12 | size: number; 13 | folder: string; 14 | path: string; 15 | tags: string[]; 16 | links: string[]; 17 | }; 18 | status: string; 19 | priority: number; 20 | tags: string[]; 21 | price: number; 22 | age: number; 23 | has_coords: boolean; 24 | lat: number; 25 | long: number; 26 | author: string; 27 | created: Date; 28 | summary: string; 29 | wordCount: number; 30 | category: string; 31 | [key: string]: any; 32 | } 33 | 34 | let fakerInstance: any = null; 35 | 36 | async function getFaker() { 37 | if (!fakerInstance) { 38 | const { faker } = await import('@faker-js/faker'); 39 | fakerInstance = faker; 40 | } 41 | return fakerInstance; 42 | } 43 | 44 | /** 45 | * Generate mock files with realistic data using Faker.js 46 | */ 47 | export async function generateMockFiles(count: number = 20): Promise { 48 | const faker = await getFaker(); 49 | 50 | const statuses = ["Todo", "In Progress", "Done", "Cancelled"]; 51 | const extensions = [".md", ".txt", ".pdf", ".docx"]; 52 | 53 | return Array.from({ length: count }, (_, index): MockFile => { 54 | const ext = extensions[Math.floor(Math.random() * extensions.length)]; 55 | const tags = Array.from( 56 | { length: Math.floor(Math.random() * 4) + 1 }, 57 | () => faker.lorem.word() 58 | ); 59 | 60 | // Random links 61 | const possibleLinks = ["Home", "Projects", "Books", "Reading", "Textbook"]; 62 | const links = Array.from( 63 | { length: Math.floor(Math.random() * 3) }, 64 | () => possibleLinks[Math.floor(Math.random() * possibleLinks.length)] 65 | ); 66 | 67 | // Base file properties 68 | const fileName = faker.system.fileName().replace(/\.[^/.]+$/, "") + ".md"; 69 | 70 | return { 71 | file: { 72 | file: `file_${index}`, 73 | name: fileName, 74 | ctime: faker.date.past(), 75 | mtime: faker.date.recent(), 76 | ext, 77 | size: faker.number.int({ min: 1024, max: 1024 * 1024 * 10 }), 78 | folder: faker.system.directoryPath(), 79 | path: faker.system.filePath().replace(/\.[^/.]+$/, ext), 80 | tags, 81 | links, 82 | }, 83 | status: statuses[Math.floor(Math.random() * statuses.length)], 84 | priority: faker.number.int({ min: 1, max: 5 }), 85 | tags, 86 | price: faker.number.float({ min: 0, max: 1000, fractionDigits: 2 }), 87 | age: faker.number.int({ min: 1, max: 100 }), 88 | has_coords: Math.random() > 0.7, 89 | lat: faker.location.latitude(), 90 | long: faker.location.longitude(), 91 | author: faker.person.fullName(), 92 | created: faker.date.past(), 93 | summary: faker.lorem.paragraph(), 94 | wordCount: faker.number.int({ min: 100, max: 10000 }), 95 | category: ["Reference", "Project", "Daily Note", "Meeting"][ 96 | Math.floor(Math.random() * 4) 97 | ], 98 | }; 99 | }); 100 | } 101 | 102 | /** 103 | * Generate book-specific mock data 104 | */ 105 | export async function generateBookData(count: number = 10): Promise { 106 | const faker = await getFaker(); 107 | 108 | const bookStatuses = ["Todo", "In Progress", "Done"]; 109 | const bookGenres = ["Fiction", "Non-Fiction", "Biography", "History", "Science", "Technology"]; 110 | 111 | return Array.from({ length: count }, (_, i): MockFile => { 112 | const status = bookStatuses[Math.floor(Math.random() * bookStatuses.length)]; 113 | const genre = bookGenres[Math.floor(Math.random() * bookGenres.length)]; 114 | const wordCount = faker.number.int({ min: 20000, max: 150000 }); 115 | const readingProgress = status === "Done" ? 100 : Math.floor(Math.random() * 100); 116 | 117 | const tags = [ 118 | "book", 119 | genre.toLowerCase(), 120 | ...(Math.random() > 0.5 ? ["favorite"] : []), 121 | ]; 122 | 123 | // Some books should link to Textbook for our filter to work 124 | const links = ["Books", "Reading"]; 125 | if (Math.random() > 0.5) { 126 | links.push("Textbook"); 127 | } 128 | 129 | return { 130 | file: { 131 | file: `book_${i}`, 132 | name: faker.lorem.words(3) + ".md", 133 | ctime: faker.date.past(), 134 | mtime: faker.date.recent(), 135 | ext: ".md", 136 | size: faker.number.int({ min: 5000, max: 50000 }), 137 | folder: "/Books", 138 | path: `/Books/${faker.lorem.words(3)}.md`, 139 | tags, 140 | links, 141 | }, 142 | status, 143 | priority: faker.number.int({ min: 1, max: 5 }), 144 | tags, 145 | price: faker.number.float({ min: 10, max: 50, fractionDigits: 2 }), 146 | age: faker.number.int({ min: 1, max: 10 }), 147 | has_coords: false, 148 | lat: 0, 149 | long: 0, 150 | author: faker.person.fullName(), 151 | created: faker.date.past(), 152 | summary: faker.lorem.paragraph(), 153 | wordCount, 154 | category: "Book", 155 | genre, 156 | readingProgress, 157 | rating: faker.number.int({ min: 1, max: 5 }), 158 | dateStarted: faker.date.past(), 159 | }; 160 | }); 161 | } 162 | 163 | /** 164 | * Generate a specific number of files with known properties for demonstrations 165 | */ 166 | export async function generateDemoFiles(): Promise { 167 | const baseFiles = await generateMockFiles(15); 168 | const bookFiles = await generateBookData(15); 169 | 170 | return [...baseFiles, ...bookFiles]; 171 | } 172 | -------------------------------------------------------------------------------- /src/lib/templateExamples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Collection of example Base templates to demonstrate different features 3 | */ 4 | 5 | export const baseTemplates = { 6 | basic: { 7 | name: "Basic Table", 8 | yaml: `filters: 9 | or: 10 | - taggedWith(file.file, "book") 11 | - and: 12 | - taggedWith(file.file, "book") 13 | - linksTo(file.file, "Textbook") 14 | formulas: 15 | formatted_price: 'concat(price, " dollars")' 16 | ppu: "price / age" 17 | display: 18 | status: Status 19 | formula.formatted_price: "Price" 20 | "file.ext": Extension 21 | views: 22 | - type: table 23 | name: "My table" 24 | limit: 10 25 | filters: 26 | and: 27 | - 'status != "done"' 28 | - or: 29 | - "formula.ppu > 5" 30 | - "price > 2.1" 31 | order: 32 | - file.name 33 | - file.ext 34 | - property.age 35 | - formula.ppu 36 | - formula.formatted_price`, 37 | }, 38 | 39 | bookLibrary: { 40 | name: "Book Library", 41 | yaml: `filters: 42 | and: 43 | - taggedWith(file.file, "book") 44 | - not: 45 | - taggedWith(file.file, "textbook") 46 | formulas: 47 | read_status: 'if(status == "Done", "Read", "Unread")' 48 | reading_time: 'wordCount / 250' 49 | display: 50 | author: Author 51 | formula.read_status: "Reading Status" 52 | formula.reading_time: "Est. Reading Hours" 53 | tags: Tags 54 | priority: Priority 55 | views: 56 | - type: table 57 | name: "Reading List" 58 | filters: 'formula.read_status == "Unread"' 59 | order: 60 | - file.name 61 | - author 62 | - priority 63 | - formula.reading_time 64 | - type: gallery 65 | name: "Book Gallery" 66 | filters: 'formula.read_status == "Unread"' 67 | title_field: file.name 68 | description_field: summary 69 | - type: board 70 | name: "Reading Board" 71 | group_by: "priority" 72 | title_field: file.name 73 | description_field: summary 74 | - type: table 75 | name: "Completed Books" 76 | filters: 'formula.read_status == "Read"' 77 | order: 78 | - file.name 79 | - author 80 | - priority 81 | - formula.reading_time`, 82 | }, 83 | 84 | projectTracker: { 85 | name: "Project Tracker", 86 | yaml: `filters: 87 | and: 88 | - taggedWith(file.file, "project") 89 | formulas: 90 | days_active: 'if(status == "In Progress", date_diff(now(), created) / 86400000, 0)' 91 | status_emoji: 'if(status == "Done", "✅", if(status == "In Progress", "🔄", if(status == "Backlog", "📌", "❓")))' 92 | display: 93 | formula.status_emoji: "" 94 | status: Status 95 | priority: Priority 96 | category: Category 97 | formula.days_active: "Days Active" 98 | views: 99 | - type: board 100 | name: "Project Board" 101 | group_by: "status" 102 | title_field: file.name 103 | description_field: summary 104 | - type: table 105 | name: "Active Projects" 106 | filters: 'status == "In Progress"' 107 | order: 108 | - priority 109 | - file.name 110 | - formula.days_active 111 | - type: table 112 | name: "Backlog" 113 | filters: 'status == "Backlog"' 114 | order: 115 | - priority 116 | - file.name`, 117 | }, 118 | 119 | locationTracker: { 120 | name: "Location Tracker", 121 | yaml: `filters: 122 | and: 123 | - has_coords == true 124 | formulas: 125 | location_name: 'concat(file.name, " (", lat, ", ", long, ")")' 126 | display: 127 | formula.location_name: "Location" 128 | summary: "Description" 129 | category: "Category" 130 | views: 131 | - type: map 132 | name: "All Locations" 133 | lat: lat 134 | long: long 135 | title: file.name 136 | - type: table 137 | name: "Location Details" 138 | order: 139 | - formula.location_name 140 | - category 141 | - summary`, 142 | }, 143 | 144 | taskManagement: { 145 | name: "Task Management", 146 | yaml: `filters: 147 | or: 148 | - category == "Project" 149 | - taggedWith(file.file, "task") 150 | formulas: 151 | due_soon: 'if(created > dateModify(now(), "-7 days"), true, false)' 152 | priority_label: 'if(priority == 5, "Critical", if(priority == 4, "High", if(priority == 3, "Medium", if(priority == 2, "Low", "None"))))' 153 | display: 154 | status: "Status" 155 | formula.priority_label: "Priority" 156 | author: "Assigned To" 157 | formula.due_soon: "Due Soon" 158 | views: 159 | - type: board 160 | name: "Task Board" 161 | group_by: "status" 162 | title_field: file.name 163 | description_field: summary 164 | - type: table 165 | name: "All Tasks" 166 | group_by: "status" 167 | order: 168 | - formula.priority_label 169 | - file.name 170 | - author 171 | - type: calendar 172 | name: "Task Calendar" 173 | date_field: created 174 | title_field: file.name 175 | - type: table 176 | name: "Due Soon" 177 | filters: 'formula.due_soon == true' 178 | order: 179 | - formula.priority_label 180 | - status 181 | - file.name`, 182 | }, 183 | 184 | readingPlanner: { 185 | name: "Reading Planner", 186 | yaml: `filters: 187 | taggedWith(file.file, "book") 188 | formulas: 189 | read_percentage: 'readingProgress' 190 | time_to_finish: 'if(readingProgress < 100, wordCount * (100 - readingProgress) / 100 / 250, 0)' 191 | read_status: 'if(readingProgress == 100, "Finished", if(readingProgress > 0, "In Progress", "Not Started"))' 192 | star_rating: 'if(rating == 5, "★★★★★", if(rating == 4, "★★★★☆", if(rating == 3, "★★★☆☆", if(rating == 2, "★★☆☆☆", if(rating == 1, "★☆☆☆☆", "Not rated")))))' 193 | display: 194 | author: "Author" 195 | genre: "Genre" 196 | readingProgress: "Progress" 197 | formula.read_status: "Status" 198 | formula.time_to_finish: "Hours Left" 199 | formula.star_rating: "Rating" 200 | views: 201 | - type: gallery 202 | name: "Book Collection" 203 | title_field: file.name 204 | description_field: summary 205 | - type: board 206 | name: "Reading Status" 207 | group_by: "formula.read_status" 208 | title_field: file.name 209 | description_field: summary 210 | - type: table 211 | name: "Reading List" 212 | filters: 'readingProgress < 100' 213 | order: 214 | - readingProgress 215 | - author 216 | - genre 217 | - type: table 218 | name: "Finished Books" 219 | filters: 'readingProgress == 100' 220 | order: 221 | - rating 222 | - author 223 | - genre 224 | - type: calendar 225 | name: "Reading Timeline" 226 | date_field: dateStarted 227 | title_field: file.name`, 228 | }, 229 | }; 230 | -------------------------------------------------------------------------------- /src/dataview-parser/normalize.ts: -------------------------------------------------------------------------------- 1 | import emojiRegex from "emoji-regex"; 2 | import P from "parsimmon"; 3 | 4 | /** The Obsidian 'link', used for uniquely describing a file, header, or block. */ 5 | export class Link { 6 | /** The file path this link points to. */ 7 | public path!: string; 8 | /** The display name associated with the link. */ 9 | public display?: string; 10 | /** The block ID or header this link points to within a file, if relevant. */ 11 | public subpath?: string; 12 | /** Is this link an embedded link (!)? */ 13 | public embed!: boolean; 14 | /** The type of this link, which determines what 'subpath' refers to, if anything. */ 15 | public type!: "file" | "header" | "block"; 16 | 17 | /** Create a link to a specific file. */ 18 | public static file(path: string, embed: boolean = false, display?: string) { 19 | return new Link({ 20 | path, 21 | embed, 22 | display, 23 | subpath: undefined, 24 | type: "file", 25 | }); 26 | } 27 | 28 | public static infer( 29 | linkpath: string, 30 | embed: boolean = false, 31 | display?: string 32 | ) { 33 | if (linkpath.includes("#^")) { 34 | let split = linkpath.split("#^"); 35 | return Link.block(split[0], split[1], embed, display); 36 | } else if (linkpath.includes("#")) { 37 | let split = linkpath.split("#"); 38 | return Link.header(split[0], split[1], embed, display); 39 | } else return Link.file(linkpath, embed, display); 40 | } 41 | 42 | /** Create a link to a specific file and header in that file. */ 43 | public static header( 44 | path: string, 45 | header: string, 46 | embed?: boolean, 47 | display?: string 48 | ) { 49 | // Headers need to be normalized to alpha-numeric & with extra spacing removed. 50 | return new Link({ 51 | path, 52 | embed, 53 | display, 54 | subpath: normalizeHeaderForLink(header), 55 | type: "header", 56 | }); 57 | } 58 | 59 | /** Create a link to a specific file and block in that file. */ 60 | public static block( 61 | path: string, 62 | blockId: string, 63 | embed?: boolean, 64 | display?: string 65 | ) { 66 | return new Link({ 67 | path, 68 | embed, 69 | display, 70 | subpath: blockId, 71 | type: "block", 72 | }); 73 | } 74 | 75 | public static fromObject(object: Record) { 76 | return new Link(object); 77 | } 78 | 79 | private constructor(fields: Partial) { 80 | Object.assign(this, fields); 81 | } 82 | 83 | /** Checks for link equality (i.e., that the links are pointing to the same exact location). */ 84 | public equals(other: Link): boolean { 85 | if (other == undefined || other == null) return false; 86 | 87 | return ( 88 | this.path == other.path && 89 | this.type == other.type && 90 | this.subpath == other.subpath 91 | ); 92 | } 93 | 94 | /** Convert this link to it's markdown representation. */ 95 | public toString(): string { 96 | return this.markdown(); 97 | } 98 | 99 | /** Convert this link to a raw object which is serialization-friendly. */ 100 | public toObject(): Record { 101 | return { 102 | path: this.path, 103 | type: this.type, 104 | subpath: this.subpath, 105 | display: this.display, 106 | embed: this.embed, 107 | }; 108 | } 109 | 110 | /** Update this link with a new path. */ 111 | //@ts-ignore; error appeared after updating Obsidian to 0.15.4; it also updated other packages but didn't say which 112 | public withPath(path: string) { 113 | return new Link(Object.assign({}, this, { path })); 114 | } 115 | 116 | /** Return a new link which points to the same location but with a new display value. */ 117 | public withDisplay(display?: string) { 118 | return new Link(Object.assign({}, this, { display })); 119 | } 120 | 121 | /** Convert a file link into a link to a specific header. */ 122 | public withHeader(header: string) { 123 | return Link.header(this.path, header, this.embed, this.display); 124 | } 125 | 126 | /** Convert any link into a link to its file. */ 127 | public toFile() { 128 | return Link.file(this.path, this.embed, this.display); 129 | } 130 | 131 | /** Convert this link into an embedded link. */ 132 | public toEmbed(): Link { 133 | if (this.embed) { 134 | return this; 135 | } else { 136 | let link = new Link(this); 137 | link.embed = true; 138 | return link; 139 | } 140 | } 141 | 142 | /** Convert this link into a non-embedded link. */ 143 | public fromEmbed(): Link { 144 | if (!this.embed) { 145 | return this; 146 | } else { 147 | let link = new Link(this); 148 | link.embed = false; 149 | return link; 150 | } 151 | } 152 | 153 | /** Convert this link to markdown so it can be rendered. */ 154 | public markdown(): string { 155 | let result = (this.embed ? "!" : "") + "[[" + this.obsidianLink(); 156 | 157 | if (this.display) { 158 | result += "|" + this.display; 159 | } else { 160 | result += "|" + getFileTitle(this.path); 161 | if (this.type == "header" || this.type == "block") 162 | result += " > " + this.subpath; 163 | } 164 | 165 | result += "]]"; 166 | return result; 167 | } 168 | 169 | /** Convert the inner part of the link to something that Obsidian can open / understand. */ 170 | public obsidianLink(): string { 171 | const escaped = this.path.replaceAll("|", "\\|"); 172 | if (this.type == "header") 173 | return escaped + "#" + this.subpath?.replaceAll("|", "\\|"); 174 | if (this.type == "block") 175 | return escaped + "#^" + this.subpath?.replaceAll("|", "\\|"); 176 | else return escaped; 177 | } 178 | 179 | /** The stripped name of the file this link points to. */ 180 | public fileName(): string { 181 | return getFileTitle(this.path).replace(".md", ""); 182 | } 183 | } 184 | 185 | /** Get the "title" for a file, by stripping other parts of the path as well as the extension. */ 186 | export function getFileTitle(path: string): string { 187 | if (path.includes("/")) path = path.substring(path.lastIndexOf("/") + 1); 188 | if (path.endsWith(".md")) path = path.substring(0, path.length - 3); 189 | return path; 190 | } 191 | 192 | const HEADER_CANONICALIZER: P.Parser = P.alt( 193 | P.regex(new RegExp(emojiRegex(), "")), 194 | P.regex(/[0-9\p{Letter}_-]+/u), 195 | P.whitespace.map((_) => " "), 196 | P.any.map((_) => " ") 197 | ) 198 | .many() 199 | .map((result) => { 200 | return result.join("").split(/\s+/).join(" ").trim(); 201 | }); 202 | 203 | export function normalizeHeaderForLink(header: string): string { 204 | return HEADER_CANONICALIZER.tryParse(header); 205 | } 206 | -------------------------------------------------------------------------------- /src/dataview-parser/query-types.ts: -------------------------------------------------------------------------------- 1 | /** Provides an AST for complex queries. */ 2 | import type { Source } from "./source-types"; 3 | import type { Field } from "./field"; 4 | 5 | /** The supported query types (corresponding to view types). */ 6 | export type QueryType = "list" | "table" | "task" | "calendar"; 7 | 8 | /** A single-line comment. */ 9 | export type Comment = string; 10 | 11 | /** Fields used in the query portion. */ 12 | export interface NamedField { 13 | /** The effective name of this field. */ 14 | name: string; 15 | /** The value of this field. */ 16 | field: Field; 17 | } 18 | 19 | /** A query sort by field, for determining sort order. */ 20 | export interface QuerySortBy { 21 | /** The field to sort on. */ 22 | field: Field; 23 | /** The direction to sort in. */ 24 | direction: "ascending" | "descending"; 25 | } 26 | 27 | /** Utility functions for quickly creating fields. */ 28 | export namespace QueryFields { 29 | export function named(name: string, field: Field): NamedField { 30 | return { name, field } as NamedField; 31 | } 32 | 33 | export function sortBy( 34 | field: Field, 35 | dir: "ascending" | "descending" 36 | ): QuerySortBy { 37 | return { field, direction: dir }; 38 | } 39 | } 40 | 41 | ////////////////////// 42 | // Query Definition // 43 | ////////////////////// 44 | 45 | /** A query which should render a list of elements. */ 46 | export interface ListQuery { 47 | type: "list"; 48 | /** What should be rendered in the list. */ 49 | format?: Field; 50 | /** If true, show the default DI field; otherwise, don't. */ 51 | showId: boolean; 52 | } 53 | 54 | /** A query which renders a table of elements. */ 55 | export interface TableQuery { 56 | type: "table"; 57 | /** The fields (computed or otherwise) to select. */ 58 | fields: NamedField[]; 59 | /** If true, show the default ID field; otherwise, don't. */ 60 | showId: boolean; 61 | } 62 | 63 | /** A query which renders a collection of tasks. */ 64 | export interface TaskQuery { 65 | type: "task"; 66 | } 67 | 68 | /** A query which renders a collection of notes in a calendar view. */ 69 | export interface CalendarQuery { 70 | type: "calendar"; 71 | /** The date field that we'll be grouping notes by for the calendar view */ 72 | field: NamedField; 73 | } 74 | 75 | export type QueryHeader = ListQuery | TableQuery | TaskQuery | CalendarQuery; 76 | 77 | /** A step which only retains rows whose 'clause' field is truthy. */ 78 | export interface WhereStep { 79 | type: "where"; 80 | clause: Field; 81 | } 82 | 83 | /** A step which sorts all current rows by the given list of sorts. */ 84 | export interface SortByStep { 85 | type: "sort"; 86 | fields: QuerySortBy[]; 87 | } 88 | 89 | /** A step which truncates the number of rows to the given amount. */ 90 | export interface LimitStep { 91 | type: "limit"; 92 | amount: Field; 93 | } 94 | 95 | /** A step which flattens rows into multiple child rows. */ 96 | export interface FlattenStep { 97 | type: "flatten"; 98 | field: NamedField; 99 | } 100 | 101 | /** A step which groups rows into groups by the given field. */ 102 | export interface GroupStep { 103 | type: "group"; 104 | field: NamedField; 105 | } 106 | 107 | /** A virtual step which extracts an array of values from each row. */ 108 | export interface ExtractStep { 109 | type: "extract"; 110 | fields: Record; 111 | } 112 | 113 | export type QueryOperation = 114 | | WhereStep 115 | | SortByStep 116 | | LimitStep 117 | | FlattenStep 118 | | GroupStep 119 | | ExtractStep; 120 | 121 | /** 122 | * A query over the Obsidian database. Queries have a specific and deterministic execution order: 123 | */ 124 | export interface Query { 125 | /** The view type to render this query in. */ 126 | header: QueryHeader; 127 | /** The source that file candidates will come from. */ 128 | source: Source; 129 | /** The operations to apply to the data to produce the final result that will be rendered. */ 130 | operations: QueryOperation[]; 131 | } 132 | 133 | /** Functional return type for error handling. */ 134 | export class Success { 135 | public successful: true; 136 | 137 | public constructor(public value: T) { 138 | this.successful = true; 139 | } 140 | 141 | public map(f: (a: T) => U): Result { 142 | return new Success(f(this.value)); 143 | } 144 | 145 | public flatMap(f: (a: T) => Result): Result { 146 | return f(this.value); 147 | } 148 | 149 | public mapErr(f: (e: E) => U): Result { 150 | return this as any as Result; 151 | } 152 | 153 | public bimap( 154 | succ: (a: T) => T2, 155 | _fail: (b: E) => E2 156 | ): Result { 157 | return this.map(succ) as any; 158 | } 159 | 160 | public orElse(_value: T): T { 161 | return this.value; 162 | } 163 | 164 | public cast(): Result { 165 | return this as any; 166 | } 167 | 168 | public orElseThrow(_message?: (e: E) => string): T { 169 | return this.value; 170 | } 171 | } 172 | 173 | /** Functional return type for error handling. */ 174 | export class Failure { 175 | public successful: false; 176 | 177 | public constructor(public error: E) { 178 | this.successful = false; 179 | } 180 | 181 | public map(_f: (a: T) => U): Result { 182 | return this as any as Failure; 183 | } 184 | 185 | public flatMap(_f: (a: T) => Result): Result { 186 | return this as any as Failure; 187 | } 188 | 189 | public mapErr(f: (e: E) => U): Result { 190 | return new Failure(f(this.error)); 191 | } 192 | 193 | public bimap( 194 | _succ: (a: T) => T2, 195 | fail: (b: E) => E2 196 | ): Result { 197 | return this.mapErr(fail) as any; 198 | } 199 | 200 | public orElse(value: T): T { 201 | return value; 202 | } 203 | 204 | public cast(): Result { 205 | return this as any; 206 | } 207 | 208 | public orElseThrow(message?: (e: E) => string): T { 209 | if (message) throw new Error(message(this.error)); 210 | else throw new Error("" + this.error); 211 | } 212 | } 213 | 214 | export type Result = Success | Failure; 215 | 216 | /** Monadic 'Result' type which encapsulates whether a procedure succeeded or failed, as well as it's return value. */ 217 | export namespace Result { 218 | /** Construct a new success result wrapping the given value. */ 219 | export function success(value: T): Result { 220 | return new Success(value); 221 | } 222 | 223 | /** Construct a new failure value wrapping the given error. */ 224 | export function failure(error: E): Result { 225 | return new Failure(error); 226 | } 227 | 228 | /** Join two results with a bi-function and return a new result. */ 229 | export function flatMap2( 230 | first: Result, 231 | second: Result, 232 | f: (a: T1, b: T2) => Result 233 | ): Result { 234 | if (first.successful) { 235 | if (second.successful) return f(first.value, second.value); 236 | else return failure(second.error); 237 | } else { 238 | return failure(first.error); 239 | } 240 | } 241 | 242 | /** Join two results with a bi-function and return a new result. */ 243 | export function map2( 244 | first: Result, 245 | second: Result, 246 | f: (a: T1, b: T2) => O 247 | ): Result { 248 | return flatMap2(first, second, (a, b) => success(f(a, b))); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/lib/CalendarView.svelte: -------------------------------------------------------------------------------- 1 | 117 | 118 |
119 |
120 |
121 | 122 |

{monthName} {currentYear}

123 | 124 |
125 | 126 |
127 | 128 |
129 |
130 | {#each dayNames as day} 131 |
{day}
132 | {/each} 133 |
134 | 135 |
136 | {#each calendarDays as { date, day, isCurrentMonth }, i} 137 |
138 |
{day}
139 | {#if hasFilesForDate(date)} 140 |
141 | {#each getDisplayFilesForDate(date) as file} 142 |
143 | {getPropertyValue(file, titleField)} 144 |
145 | {/each} 146 | 147 | {#if getFilesForDate(date).length > 3} 148 |
+{getFilesForDate(date).length - 3} more
149 | {/if} 150 |
151 | {/if} 152 |
153 | {/each} 154 |
155 |
156 |
157 | 158 | -------------------------------------------------------------------------------- /src/dataview-parser/query-parse.ts: -------------------------------------------------------------------------------- 1 | import { EXPRESSION } from "./expression-parse"; 2 | import P from "parsimmon"; 3 | import type { 4 | FlattenStep, 5 | GroupStep, 6 | LimitStep, 7 | NamedField, 8 | Query, 9 | QueryHeader, 10 | QueryOperation, 11 | QuerySortBy, 12 | QueryType, 13 | SortByStep, 14 | WhereStep, 15 | Comment, 16 | } from "./query-types"; 17 | import { QueryFields } from "./query-types"; 18 | import type { Source } from "./source-types"; 19 | import { Sources } from "./source-types"; 20 | import { Result } from "./query-types"; 21 | 22 | /////////////////// 23 | // Query Parsing // 24 | /////////////////// 25 | 26 | /** Typings for the outputs of all of the parser combinators. */ 27 | interface QueryLanguageTypes { 28 | queryType: QueryType; 29 | comment: Comment; 30 | 31 | explicitNamedField: NamedField; 32 | namedField: NamedField; 33 | sortField: QuerySortBy; 34 | 35 | // Entire clauses in queries. 36 | headerClause: QueryHeader; 37 | fromClause: Source; 38 | whereClause: WhereStep; 39 | sortByClause: SortByStep; 40 | limitClause: LimitStep; 41 | flattenClause: FlattenStep; 42 | groupByClause: GroupStep; 43 | clause: QueryOperation; 44 | query: Query; 45 | } 46 | 47 | /** Return a new parser which executes the underlying parser and returns it's raw string representation. */ 48 | export function captureRaw(base: P.Parser): P.Parser<[T, string]> { 49 | return P.custom((success, failure) => { 50 | return (input, i) => { 51 | let result = (base as any)._(input, i); 52 | if (!result.status) return result; 53 | 54 | return Object.assign({}, result, { 55 | value: [result.value, input.substring(i, result.index)], 56 | }); 57 | }; 58 | }); 59 | } 60 | 61 | /** Strip newlines and excess whitespace out of text. */ 62 | function stripNewlines(text: string): string { 63 | return text 64 | .split(/[\r\n]+/) 65 | .map((t) => t.trim()) 66 | .join(""); 67 | } 68 | 69 | /** Given `parser`, return the parser that returns `if_eof()` if EOF is found, 70 | * otherwise `parser` preceded by (non-optional) whitespace */ 71 | function precededByWhitespaceIfNotEof( 72 | if_eof: (_: undefined) => T, 73 | parser: P.Parser 74 | ): P.Parser { 75 | return P.eof.map(if_eof).or(P.whitespace.then(parser)); 76 | } 77 | 78 | /** A parsimmon-powered parser-combinator implementation of the query language. */ 79 | export const QUERY_LANGUAGE = P.createLanguage({ 80 | // Simple atom parsing, like words, identifiers, numbers. 81 | queryType: (q) => 82 | P.alt(P.regexp(/TABLE|LIST|TASK|CALENDAR/i)) 83 | .map((str) => str.toLowerCase() as QueryType) 84 | .desc("query type ('TABLE', 'LIST', 'TASK', or 'CALENDAR')"), 85 | explicitNamedField: (q) => 86 | P.seqMap( 87 | EXPRESSION.field.skip(P.whitespace), 88 | P.regexp(/AS/i).skip(P.whitespace), 89 | EXPRESSION.identifier.or(EXPRESSION.string), 90 | (field, _as, ident) => QueryFields.named(ident, field) 91 | ), 92 | comment: () => 93 | P.Parser((input, i) => { 94 | // Parse a comment, which is a line starting with //. 95 | let line = input.substring(i); 96 | if (!line.startsWith("//")) return P.makeFailure(i, "Not a comment"); 97 | // The comment ends at the end of the line. 98 | line = line.split("\n")[0]; 99 | let comment = line.substring(2).trim(); 100 | return P.makeSuccess(i + line.length, comment); 101 | }), 102 | namedField: (q) => 103 | P.alt( 104 | q.explicitNamedField, 105 | captureRaw(EXPRESSION.field).map(([value, text]) => 106 | QueryFields.named(stripNewlines(text), value) 107 | ) 108 | ), 109 | sortField: (q) => 110 | P.seqMap( 111 | EXPRESSION.field.skip(P.optWhitespace), 112 | P.regexp(/ASCENDING|DESCENDING|ASC|DESC/i).atMost(1), 113 | (field, dir) => { 114 | let direction = dir.length == 0 ? "ascending" : dir[0].toLowerCase(); 115 | if (direction == "desc") direction = "descending"; 116 | if (direction == "asc") direction = "ascending"; 117 | return { 118 | field: field, 119 | direction: direction as "ascending" | "descending", 120 | }; 121 | } 122 | ), 123 | 124 | headerClause: (q) => 125 | q.queryType 126 | .chain((type): P.Parser => { 127 | switch (type) { 128 | case "table": { 129 | return precededByWhitespaceIfNotEof( 130 | () => ({ type, fields: [], showId: true }), 131 | P.seqMap( 132 | P.regexp(/WITHOUT\s+ID/i) 133 | .skip(P.optWhitespace) 134 | .atMost(1), 135 | P.sepBy(q.namedField, P.string(",").trim(P.optWhitespace)), 136 | (withoutId, fields) => { 137 | return { type, fields, showId: withoutId.length == 0 }; 138 | } 139 | ) 140 | ); 141 | } 142 | case "list": 143 | return precededByWhitespaceIfNotEof( 144 | () => ({ type, format: undefined, showId: true }), 145 | P.seqMap( 146 | P.regexp(/WITHOUT\s+ID/i) 147 | .skip(P.optWhitespace) 148 | .atMost(1), 149 | EXPRESSION.field.atMost(1), 150 | (withoutId, format) => { 151 | return { 152 | type, 153 | format: format.length == 1 ? format[0] : undefined, 154 | showId: withoutId.length == 0, 155 | }; 156 | } 157 | ) 158 | ); 159 | case "task": 160 | return P.succeed({ type }); 161 | case "calendar": 162 | return P.whitespace.then( 163 | P.seqMap(q.namedField, (field) => { 164 | return { 165 | type, 166 | showId: true, 167 | field, 168 | } as QueryHeader; 169 | }) 170 | ); 171 | default: 172 | return P.fail(`Unrecognized query type '${type}'`); 173 | } 174 | }) 175 | .desc("TABLE or LIST or TASK or CALENDAR"), 176 | fromClause: (q) => 177 | P.seqMap( 178 | P.regexp(/FROM/i), 179 | P.whitespace, 180 | EXPRESSION.source, 181 | (_1, _2, source) => source 182 | ), 183 | whereClause: (q) => 184 | P.seqMap( 185 | P.regexp(/WHERE/i), 186 | P.whitespace, 187 | EXPRESSION.field, 188 | (where, _, field) => { 189 | return { type: "where", clause: field } as WhereStep; 190 | } 191 | ).desc("WHERE "), 192 | sortByClause: (q) => 193 | P.seqMap( 194 | P.regexp(/SORT/i), 195 | P.whitespace, 196 | q.sortField.sepBy1(P.string(",").trim(P.optWhitespace)), 197 | (sort, _1, fields) => { 198 | return { type: "sort", fields } as SortByStep; 199 | } 200 | ).desc("SORT field [ASC/DESC]"), 201 | limitClause: (q) => 202 | P.seqMap( 203 | P.regexp(/LIMIT/i), 204 | P.whitespace, 205 | EXPRESSION.field, 206 | (limit, _1, field) => { 207 | return { type: "limit", amount: field } as LimitStep; 208 | } 209 | ).desc("LIMIT "), 210 | flattenClause: (q) => 211 | P.seqMap( 212 | P.regexp(/FLATTEN/i).skip(P.whitespace), 213 | q.namedField, 214 | (_, field) => { 215 | return { type: "flatten", field } as FlattenStep; 216 | } 217 | ).desc("FLATTEN [AS ]"), 218 | groupByClause: (q) => 219 | P.seqMap( 220 | P.regexp(/GROUP BY/i).skip(P.whitespace), 221 | q.namedField, 222 | (_, field) => { 223 | return { type: "group", field } as GroupStep; 224 | } 225 | ).desc("GROUP BY [AS ]"), 226 | // Full query parsing. 227 | clause: (q) => 228 | P.alt( 229 | q.fromClause, 230 | q.whereClause, 231 | q.sortByClause, 232 | q.limitClause, 233 | q.groupByClause, 234 | q.flattenClause 235 | ), 236 | query: (q) => 237 | P.seqMap( 238 | q.headerClause.trim(optionalWhitespaceOrComment), 239 | q.fromClause.trim(optionalWhitespaceOrComment).atMost(1), 240 | q.clause.trim(optionalWhitespaceOrComment).many(), 241 | (header, from, clauses) => { 242 | return { 243 | header, 244 | source: from.length == 0 ? Sources.folder("") : from[0], 245 | operations: clauses, 246 | } as Query; 247 | } 248 | ), 249 | }); 250 | 251 | /** 252 | * A parser for optional whitespace or comments. This is used to exclude whitespace and comments from other parsers. 253 | */ 254 | const optionalWhitespaceOrComment: P.Parser = P.alt( 255 | P.whitespace, 256 | QUERY_LANGUAGE.comment 257 | ) 258 | .many() // Use many() since there may be zero whitespaces or comments. 259 | // Transform the many to a single result. 260 | .map((arr) => arr.join("")); 261 | 262 | /** 263 | * Attempt to parse a query from the given query text, returning a string error 264 | * if the parse failed. 265 | */ 266 | export function parseQuery(text: string): Result { 267 | try { 268 | let query = QUERY_LANGUAGE.query.tryParse(text); 269 | return Result.success(query); 270 | } catch (error) { 271 | return Result.failure("" + error); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/lib/mockDataGenerator.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | 3 | export interface MockFile { 4 | file: { 5 | file: string; 6 | name: string; 7 | ctime: Date; 8 | mtime: Date; 9 | ext: string; 10 | size: number; 11 | folder: string; 12 | path: string; 13 | tags?: string[]; 14 | links?: string[]; 15 | }; 16 | [key: string]: any; 17 | } 18 | 19 | /** 20 | * Generate mock files with properties that might be found in Obsidian notes 21 | */ 22 | export function generateMockFiles(count: number = 100): MockFile[] { 23 | return Array.from({ length: count }, (_, i) => generateMockFile(i)); 24 | } 25 | 26 | /** 27 | * Generate a single mock file with properties 28 | */ 29 | function generateMockFile(index: number): MockFile { 30 | // Common file statuses in task management 31 | const statuses = ["TODO", "In Progress", "Done", "Backlog", "Waiting"]; 32 | 33 | // Only use markdown files 34 | const ext = ".md"; 35 | 36 | // Random tags that might be used in notes 37 | const possibleTags = [ 38 | "book", 39 | "article", 40 | "blog", 41 | "research", 42 | "project", 43 | "idea", 44 | "meeting", 45 | "review", 46 | "personal", 47 | "work", 48 | "study", 49 | "reference", 50 | "tag", 51 | "important", 52 | "urgent", 53 | "later", 54 | "textbook", 55 | ]; 56 | 57 | // Random list of tags 58 | const tags = Array.from( 59 | { length: Math.floor(Math.random() * 5) }, 60 | () => possibleTags[Math.floor(Math.random() * possibleTags.length)] 61 | ); 62 | 63 | // Random links 64 | const possibleLinks = ["Home", "Projects", "Books", "Reading", "Textbook"]; 65 | const links = Array.from( 66 | { length: Math.floor(Math.random() * 3) }, 67 | () => possibleLinks[Math.floor(Math.random() * possibleLinks.length)] 68 | ); 69 | 70 | // Base file properties 71 | const fileName = faker.system.fileName().replace(/\.[^/.]+$/, "") + ".md"; 72 | 73 | return { 74 | file: { 75 | file: `file_${index}`, // Placeholder for file object 76 | name: fileName, 77 | ctime: faker.date.past(), 78 | mtime: faker.date.recent(), 79 | ext, 80 | size: faker.number.int({ min: 1024, max: 1024 * 1024 * 10 }), 81 | folder: faker.system.directoryPath(), 82 | path: faker.system.filePath().replace(/\.[^/.]+$/, ext), 83 | tags, 84 | links, 85 | }, 86 | status: statuses[Math.floor(Math.random() * statuses.length)], 87 | priority: faker.number.int({ min: 1, max: 5 }), 88 | tags, 89 | price: faker.number.float({ min: 0, max: 1000, fractionDigits: 2 }), 90 | age: faker.number.int({ min: 1, max: 100 }), 91 | has_coords: Math.random() > 0.7, 92 | lat: faker.location.latitude(), 93 | long: faker.location.longitude(), 94 | author: faker.person.fullName(), 95 | created: faker.date.past(), 96 | summary: faker.lorem.paragraph(), 97 | wordCount: faker.number.int({ min: 100, max: 10000 }), 98 | category: ["Reference", "Project", "Daily Note", "Meeting"][ 99 | Math.floor(Math.random() * 4) 100 | ], 101 | }; 102 | } 103 | 104 | /** 105 | * Generate book data for reading list and completed books 106 | */ 107 | function generateBookData(count: number): MockFile[] { 108 | const bookStatuses = ["TODO", "In Progress", "Done"]; 109 | const bookGenres = [ 110 | "Fiction", 111 | "Non-Fiction", 112 | "Science Fiction", 113 | "Fantasy", 114 | "Biography", 115 | "History", 116 | "Self-Help", 117 | "Business", 118 | "Technology", 119 | ]; 120 | 121 | return Array.from({ length: count }, (_, i): MockFile => { 122 | const status = 123 | bookStatuses[Math.floor(Math.random() * bookStatuses.length)]; 124 | const genre = bookGenres[Math.floor(Math.random() * bookGenres.length)]; 125 | const wordCount = faker.number.int({ min: 20000, max: 150000 }); 126 | const readingProgress = 127 | status === "Done" ? 100 : Math.floor(Math.random() * 100); 128 | 129 | const tags = [ 130 | "book", 131 | genre.toLowerCase(), 132 | ...(Math.random() > 0.5 ? ["favorite"] : []), 133 | ]; 134 | 135 | // Some books should link to Textbook for our filter to work 136 | const links = ["Books", "Reading"]; 137 | if (Math.random() > 0.5) { 138 | links.push("Textbook"); 139 | } 140 | 141 | return { 142 | file: { 143 | file: `book_${i}`, 144 | name: `${faker.word.adjective()} ${faker.word.noun()} - ${faker.person.lastName()}`, 145 | ctime: faker.date.past(), 146 | mtime: faker.date.recent(), 147 | ext: ".md", 148 | size: faker.number.int({ min: 5000, max: 15000 }), 149 | folder: "/Reading/Books", 150 | path: `/Reading/Books/book_${i}.md`, 151 | tags, 152 | links, 153 | }, 154 | status, 155 | priority: faker.number.int({ min: 1, max: 5 }), 156 | price: faker.number.float({ min: 0, max: 100, fractionDigits: 2 }), 157 | age: faker.number.int({ min: 1, max: 100 }), 158 | tags, 159 | author: faker.person.fullName(), 160 | created: faker.date.past(), 161 | summary: faker.lorem.paragraph(), 162 | wordCount, 163 | category: "Reference", 164 | isbn: faker.string.numeric(13), 165 | genre, 166 | publicationYear: faker.number.int({ min: 1950, max: 2023 }), 167 | publisher: faker.company.name(), 168 | readingProgress, 169 | rating: status === "Done" ? faker.number.int({ min: 1, max: 5 }) : null, 170 | dateStarted: faker.date.past(), 171 | dateFinished: status === "Done" ? faker.date.recent() : null, 172 | }; 173 | }); 174 | } 175 | 176 | /** 177 | * Generate a specific number of files with known properties for specific demonstrations 178 | */ 179 | export function generateDemoFiles(): MockFile[] { 180 | const baseFiles = generateMockFiles(15); 181 | const bookFiles = generateBookData(15); 182 | 183 | // Create base data for formula evaluation 184 | const baseData = { 185 | formulas: { 186 | read_status: 'if(status == "Done", "Read", "Unread")', 187 | reading_time: "wordCount / 250", 188 | ppu: "price / age", 189 | }, 190 | }; 191 | 192 | // Add specific files with properties for demonstrating filters 193 | const specialFiles = [ 194 | { 195 | file: { 196 | file: "special_1", 197 | name: "1984 by George Orwell", 198 | ctime: new Date("2023-01-15"), 199 | mtime: new Date("2023-03-20"), 200 | ext: ".md", 201 | size: 15460, 202 | folder: "/Reading/Books", 203 | path: "/Reading/Books/1984.md", 204 | tags: ["book", "fiction", "classic", "dystopian"], 205 | links: ["Books", "Classics", "Textbook"], 206 | }, 207 | status: "Done", 208 | priority: 5, 209 | tags: ["book", "fiction", "classic", "dystopian"], 210 | author: "George Orwell", 211 | created: new Date("2023-01-15"), 212 | summary: 213 | "Classic dystopian novel about a society under totalitarian rule", 214 | wordCount: 88000, 215 | category: "Reference", 216 | isbn: "9780451524935", 217 | genre: "Fiction", 218 | publicationYear: 1949, 219 | publisher: "Penguin Books", 220 | readingProgress: 100, 221 | rating: 5, 222 | dateStarted: new Date("2023-01-20"), 223 | dateFinished: new Date("2023-02-15"), 224 | _baseData: baseData, 225 | }, 226 | { 227 | file: { 228 | file: "special_2", 229 | name: "The Hobbit by J.R.R. Tolkien", 230 | ctime: new Date("2023-04-10"), 231 | mtime: new Date("2023-05-10"), 232 | ext: ".md", 233 | size: 12240, 234 | folder: "/Reading/Books", 235 | path: "/Reading/Books/The_Hobbit.md", 236 | tags: ["book", "fiction", "fantasy"], 237 | links: ["Books", "Textbook", "Fantasy"], 238 | }, 239 | status: "In Progress", 240 | priority: 4, 241 | tags: ["book", "fiction", "fantasy"], 242 | author: "J.R.R. Tolkien", 243 | created: new Date("2023-04-10"), 244 | summary: 245 | "Fantasy novel about Bilbo Baggins' adventure with dwarves to reclaim their treasure", 246 | wordCount: 95000, 247 | category: "Reference", 248 | isbn: "9780547928227", 249 | genre: "Fantasy", 250 | publicationYear: 1937, 251 | publisher: "Houghton Mifflin", 252 | readingProgress: 65, 253 | rating: null, 254 | dateStarted: new Date("2023-04-15"), 255 | dateFinished: null, 256 | _baseData: baseData, 257 | }, 258 | { 259 | file: { 260 | file: "special_3", 261 | name: "Project Alpha Planning", 262 | ctime: new Date("2023-05-10"), 263 | mtime: new Date("2023-05-10"), 264 | ext: ".md", 265 | size: 8240, 266 | folder: "/Projects/Alpha", 267 | path: "/Projects/Alpha/Planning.md", 268 | tags: ["project", "planning", "important", "tag"], 269 | links: ["Projects", "Alpha", "Team"], 270 | }, 271 | status: "In Progress", 272 | priority: 4, 273 | tags: ["project", "planning", "important"], 274 | price: 0, 275 | age: 2, 276 | has_coords: true, 277 | lat: 40.7128, 278 | long: -74.006, 279 | author: "Team Lead", 280 | created: new Date("2023-05-10"), 281 | summary: "Project planning document for Alpha", 282 | wordCount: 4500, 283 | category: "Project", 284 | _baseData: baseData, 285 | }, 286 | { 287 | file: { 288 | file: "special_4", 289 | name: "Meeting Notes - Research Review", 290 | ctime: new Date("2023-06-22"), 291 | mtime: new Date("2023-06-22"), 292 | ext: ".md", 293 | size: 5130, 294 | folder: "/Meetings", 295 | path: "/Meetings/Research_Review.md", 296 | tags: ["meeting", "research", "notes", "tag"], 297 | links: ["Meetings", "Research"], 298 | }, 299 | status: "Done", 300 | priority: 3, 301 | tags: ["meeting", "research", "notes"], 302 | price: 0, 303 | age: 1, 304 | has_coords: true, 305 | lat: 37.7749, 306 | long: -122.4194, 307 | author: "Meeting Scribe", 308 | created: new Date("2023-06-22"), 309 | summary: "Notes from the research review meeting", 310 | wordCount: 2200, 311 | category: "Meeting", 312 | _baseData: baseData, 313 | }, 314 | ]; 315 | 316 | // Add _baseData to all files 317 | const allFiles = [...specialFiles, ...bookFiles, ...baseFiles].map((file) => { 318 | return { ...file, _baseData: baseData }; 319 | }); 320 | 321 | return allFiles; 322 | } 323 | -------------------------------------------------------------------------------- /docs/functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: bases/functions 3 | --- 4 | Functions are used in [[Introduction to Bases|Bases]] to manipulate data from [[Properties]] in filters and formulas. See the [[Bases syntax|bases syntax]] reference to learn more about how you can use functions. 5 | 6 | Aside from [[Functions#Global|Global]] functions, most functions depend on the type of value you want to modify: 7 | 8 | - [[Functions#Any|Any]] 9 | - [[Functions#Date|Date]] 10 | - [[Functions#String|String]] 11 | - [[Functions#Number|Number]] 12 | - [[Functions#List|List]] 13 | - [[Functions#Link|Link]] 14 | - [[Functions#File|File]] 15 | - [[Functions#Object|Object]] 16 | - [[Functions#Regular expression|Regular expression]] 17 | 18 | ## Global 19 | 20 | Global functions are used without a type. 21 | 22 | ### `date()` 23 | 24 | `date(date: string): date` 25 | 26 | - `date(string): date` parses the provided string and returns a date object. 27 | - The `date` string should be in the format `YYYY-MM-DD HH:mm:ss`. 28 | 29 | ### `if()` 30 | 31 | `if(condition: any, trueResult: any, falseResult?: any): any` 32 | 33 | - `condition` is the condition to be evaluated. 34 | - `trueResult` is the output if condition is true. 35 | - `falseResult` is the optional output if the condition is false. If it is not given, then it is assumed to be `null`. 36 | - Returns the `trueResult` if `condition` is true, or is a truthy value, or `falseResult` otherwise. 37 | - Example: `if(isModified, "Modified", "Unmodified")` 38 | 39 | ### `max()` 40 | 41 | `max(value1: number, value2: number...): number` 42 | 43 | - Returns the largest of all the provided numbers. 44 | 45 | ### `min()` 46 | 47 | `min(value1: number, value2: number...): number` 48 | 49 | - Returns the smallest of all the provided numbers. 50 | 51 | ### `link()` 52 | 53 | `link(path: string | file, display?: string): Link` 54 | 55 | - Parses a string `path` and returns a Link object that renders as a link to the path given. 56 | - Optionally provide the `display` parameter to change what text the link says. 57 | 58 | ### `list()` 59 | 60 | `list(element: any): List` 61 | 62 | - If the provided element is a list, return it unmodified. 63 | - Otherwise, wraps the provided `element` in a list, creating a list with a single element. 64 | - This function can be helpful when a property contains a mixture of strings or lists across the vault. 65 | - Example: `list("value")` returns `["value"]`. 66 | 67 | ### `now()` 68 | 69 | `now(): date` 70 | 71 | - `now()` returns a date object representing the current moment. 72 | 73 | ### `number()` 74 | 75 | `number(input: any): number` 76 | 77 | - Attempt to return the provided value as a number. 78 | - Date objects will be returned as milliseconds since the unix epoch. 79 | - Booleans will return a 1 or 0. 80 | - Strings will be parsed into a number and return an error if the result is invalid. 81 | - Example, `number("3.4")` returns `3.4`. 82 | 83 | ### `today()` 84 | 85 | `today(): date` 86 | 87 | - `today()` returns a date object representing the current date. The time portion is set to zero. 88 | 89 | ## Any 90 | 91 | Functions you can use with any value. This includes strings (e.g. `"hello"`), numbers (e.g. `42`), lists (e.g. `[1,2,3]`), objects, and more. 92 | 93 | ### `toString()` 94 | 95 | `any.toString(): string` 96 | 97 | - Returns the string representation of any value. 98 | - Example: `123.toString()` returns `"123"`. 99 | 100 | 101 | ## Date 102 | 103 | Functions you can use with a date and time such as `date("2025-05-27")`. Date comparisons can be done using [[Bases syntax#Date arithmetic|date arithmetic]]. 104 | 105 | ### Fields 106 | 107 | The following fields are available for dates: 108 | 109 | | Field | Type | Description | 110 | | ------------------ | -------- | ---------------------------- | 111 | | `date.year` | `number` | The year of the date | 112 | | `date.month` | `number` | The month of the date (1–12) | 113 | | `date.day` | `number` | The day of the month | 114 | | `date.hour` | `number` | The hour (0–23) | 115 | | `date.minute` | `number` | The minute (0–59) | 116 | | `date.second` | `number` | The second (0–59) | 117 | | `date.millisecond` | `number` | The millisecond (0–999) | 118 | 119 | ### `date()` 120 | 121 | `date.date(): date` 122 | 123 | - Returns a date object with the time removed. 124 | - Example: `now().date().format("YYYY-MM-DD HH:mm:ss"` returns a string such as "2025-12-31 00:00:00" 125 | 126 | ### `format()` 127 | 128 | `date.format(format: string): string` 129 | 130 | - `format` is the format string (e.g., `"YYYY-MM-DD"`). 131 | - Returns the date formatted as specified by a Moment.js format string. 132 | - Example: `date.format("YYYY-MM-DD")` returns `"2025-05-27"`. 133 | 134 | ### `time()` 135 | 136 | `date.time(): string` 137 | 138 | - Returns the time 139 | - Example: `now().time()` returns a string such as "23:59:59" 140 | 141 | ## String 142 | 143 | Functions you can use with a sequence of characters such as `"hello".` 144 | 145 | ### Fields 146 | 147 | | Field | Type | Description | 148 | | --------------- | -------- | -------------------------------------- | 149 | | `string.length` | `number` | The number of characters in the string | 150 | 151 | ### `contains()` 152 | 153 | `string.contains(value: string): boolean` 154 | 155 | - `value` is the substring to search for. 156 | - Returns true if the string contains `value`. 157 | - Example: `"hello".contains("ell")` returns `true`. 158 | 159 | ### `containsAll()` 160 | 161 | `string.containsAll(...values: string): boolean` 162 | 163 | - `values` are one or more substrings to search for. 164 | - Returns true if the string contains all of the `values`. 165 | - Example: `"hello".containsAll("h", "e")` returns `true`. 166 | 167 | ### `containsAny()` 168 | 169 | `string.containsAny(...values: string): boolean` 170 | 171 | - `values` are one or more substrings to search for. 172 | - Returns true if the string contains at least one of the `values`. 173 | - Example: `"hello".containsAny("x", "y", "e")` returns `true`. 174 | 175 | ### `endsWith()` 176 | 177 | `string.endsWith(query: string): boolean` 178 | 179 | - `query` is the string to check at the end. 180 | - Returns true if this string ends with `query`. 181 | - Example: `"hello".endsWith("lo")` returns `true`. 182 | 183 | ### `icon()` 184 | 185 | `string.icon(): string` 186 | 187 | - Returns a string that represents the icon name to be rendered using Lucide. The icon name must match a supported Lucide icon. 188 | - Example: `"arrow-right".icon()` returns `"arrow-right"`. 189 | 190 | ### `isEmpty()` 191 | 192 | `string.isEmpty(): boolean` 193 | 194 | - Returns true if the string has no characters, or is not present. 195 | - Example: `"Hello world".isEmpty()` returns `false`. 196 | - Example: `"".isEmpty()` returns `true`. 197 | 198 | ### `replace()` 199 | 200 | `string.replace(pattern: string | Regexp, replacement: string): string` 201 | 202 | - `pattern` is the value to search for in the target string. 203 | - `replacement` is the value to replace found patterns with. 204 | - If `pattern` is a string, all occurrences of the pattern will be replaced. 205 | - If `pattern` is a Regexp, the `g` flag determines if only the first or if all occurrences are replaced. 206 | - Example: `"a,b,c,d".replace(/,/, "-")` returns `"a-b,c,d"`, where as `"a,b,c,d".replace(/,/g, "-")` returns `"a-b-c-d"`. 207 | 208 | ### `lower()` 209 | 210 | `string.lower(): string` 211 | 212 | - Returns the string converted to lower case. 213 | 214 | ### `reverse()` 215 | 216 | `string.reverse(): string` 217 | 218 | - Reverses the string. 219 | - Example: `"hello".reverse()` returns `"olleh"`. 220 | 221 | ### `slice()` 222 | 223 | `string.slice(start: number, end?: number): string` 224 | 225 | - `start` is the inclusive start index. 226 | - `end` is the optional exclusive end index. 227 | - Returns a substring from `start` (inclusive) to `end` (exclusive). 228 | - Example: `"hello".slice(1, 4)` returns `"ell"`. 229 | - If `end` is omitted, slices to the end of the string. 230 | 231 | ### `split()` 232 | 233 | `string.split(separator: string | Regexp, n?: number): list` 234 | 235 | - `separator` is the delimiter for splitting the string. 236 | - `n` is an optional number. If provided, the result will have the first `n` elements. 237 | - Returns an list of substrings. 238 | - Example: `"a,b,c,d".split(",", 3)` or `"a,b,c,d".split(/,/, 3)` returns `["a", "b", "c"]`. 239 | 240 | ### `startsWith()` 241 | 242 | `string.startsWith(query: string): boolean` 243 | 244 | - `query` is the string to check at the beginning. 245 | - Returns true if this string starts with `query`. 246 | - Example: `"hello".startsWith("he")` returns `true`. 247 | 248 | ### `title()` 249 | 250 | `string.title(): string` 251 | 252 | - Converts the string to title case (first letter of each word capitalized). 253 | - Example: `"hello world".title()` returns `"Hello World"`. 254 | 255 | ### `trim()` 256 | 257 | `string.trim(): string` 258 | 259 | - Removes whitespace from both ends of the string. 260 | - Example: `" hi ".trim()` returns `"hi"`. 261 | 262 | ## Number 263 | 264 | Functions you can use with numeric values such as `42`, `3.14`. 265 | 266 | ### `abs()` 267 | 268 | `number.abs(): number` 269 | 270 | - Returns the absolute value of the number. 271 | - Example: `(-5).abs()` returns `5`. 272 | 273 | ### `ceil()` 274 | 275 | `number.ceil(): number` 276 | 277 | - Rounds the number up to the nearest integer. 278 | - Example: `(2.1).ceil()` returns `3`. 279 | 280 | ### `floor()` 281 | 282 | `number.floor(): number` 283 | 284 | - Rounds the number down to the nearest integer. 285 | - Example: `(2.9).floor()` returns `2`. 286 | 287 | ### `round()` 288 | 289 | `number.round(digits: number): number` 290 | 291 | - Rounds the number to the nearest integer. 292 | - Optionally, provided a `digits` parameter to round to that number of decimal digits. 293 | - Example: `(2.5).round()` returns `3`, and `(2.3333).round(2)` returns `2.33`. 294 | 295 | ### `toFixed()` 296 | 297 | `number.toFixed(precision: number): string` 298 | 299 | - `precision` is the number of decimal places. 300 | - Returns a string with the number in fixed-point notation. 301 | - Example: `(3.14159).toFixed(2)` returns `"3.14"`. 302 | 303 | ### `isEmpty()` 304 | 305 | `number.isEmpty(): boolean` 306 | 307 | - Returns true if the number is not present. 308 | - Example: `5.isEmpty()` returns `false`. 309 | 310 | ## List 311 | 312 | Functions you can use with an ordered list of elements such as `[1, 2, 3]`. 313 | 314 | ### Fields 315 | 316 | | Field | Type | Description | 317 | | ------------- | -------- | ---------------------------------- | 318 | | `list.length` | `number` | The number of elements in the list | 319 | 320 | ### `contains()` 321 | 322 | `list.contains(value: any): boolean` 323 | 324 | - `value` is the element to search for. 325 | - Returns true if the list contains `value`. 326 | - Example: `[1,2,3].contains(2)` returns `true`. 327 | 328 | ### `containsAll()` 329 | 330 | `list.containsAll(...values: any): boolean` 331 | 332 | - `values` are one or more elements to search for. 333 | - Returns true if the list contains all of the `values`. 334 | - Example: `[1,2,3].containsAll(2,3)` returns `true`. 335 | 336 | ### `containsAny()` 337 | 338 | `list.containsAny(...values: any): boolean` 339 | 340 | - `values` are one or more elements to search for. 341 | - Returns true if the list contains at least one of the `values`. 342 | - Example: `[1,2,3].containsAny(3,4)` returns `true`. 343 | 344 | ### `isEmpty()` 345 | 346 | `list.isEmpty(): boolean` 347 | 348 | - Returns true if the list has no elements. 349 | - Example: `[1,2,3].isEmpty()` returns `false`. 350 | 351 | ### `join()` 352 | 353 | `list.join(separator: string): string` 354 | 355 | - `separator` is the string to insert between elements. 356 | - Joins all list elements into a single string. 357 | - Example: `[1,2,3].join(",")` returns `"1,2,3"`. 358 | 359 | ### `reverse()` 360 | 361 | `list.reverse(): list` 362 | 363 | - Reverses the list in place. 364 | - Example: `[1,2,3].reverse()` returns `[3,2,1]`. 365 | 366 | ### `sort()` 367 | 368 | `list.sort(): list` 369 | 370 | - Sorts list elements from smallest to largest. 371 | - Example: `[3, 1, 2].sort()` returns `[1, 2, 3]`. 372 | - Example: `["c", "a", "b"].sort()` returns `["a", "b", "c"]`. 373 | 374 | ### `flat()` 375 | 376 | `list.flat(): list` 377 | 378 | - Flattens nested list into a single list. 379 | - Example: `[1,[2,3]].flat()` returns `[1,2,3]`. 380 | 381 | ### `unique()` 382 | 383 | `list.unique(): list` 384 | 385 | - Removes duplicate elements. 386 | - Example: `[1,2,2,3].unique()` returns `[1,2,3]`. 387 | 388 | ### `slice()` 389 | 390 | `list.slice(start: number, end?: number): list` 391 | 392 | - `start` is the inclusive start index. 393 | - `end` is the optional exclusive end index. 394 | - Returns a shallow copy of a portion of the list from `start` (inclusive) to `end` (exclusive). 395 | - Example: `[1,2,3,4].slice(1,3)` returns `[2,3]`. 396 | - If `end` is omitted, slices to the end of the list. 397 | 398 | ## Link 399 | 400 | Functions you can use on a link. Links can be created from a file (`file.asLink()`) or a path (`link("path")`). 401 | 402 | ### `linksTo()` 403 | 404 | `link.linksTo(file): boolean` 405 | 406 | - Returns whether the file represented by the `link` has a link to `file`. 407 | 408 | ## File 409 | 410 | Functions you can use with file in the vault, such as `file("notes.md")`. 411 | 412 | ### `asLink()` 413 | 414 | `file.asLink(display?: string): Link` 415 | 416 | - `display` optional display text for the link. 417 | - Returns a Link object that renders as a functioning link. 418 | - Example: `file.asLink()` 419 | 420 | ### `hasLink()` 421 | 422 | `file.hasLink(otherFile: file | string): boolean` 423 | 424 | - `otherFile` is another file object or string path to check. 425 | - Returns true if `file` links to`otherFile`. 426 | - Example: `file.hasLink(otherFile)` returns `true` if there’s a link from `file` to `otherFile`. 427 | 428 | ### `hasTag()` 429 | 430 | `file.hasTag(...values: string): boolean` 431 | 432 | - `values` are one or more tag names. 433 | - Returns true if the file has any of the tags in `values`. 434 | - Example: `file.hasTag("tag1", "tag2")` returns `true` if the file has either tag. 435 | 436 | ### `inFolder()` 437 | 438 | `file.inFolder(folder: string): boolean` 439 | 440 | - `folder` is the folder name to check. 441 | - Returns true if the file is in the specified folder. 442 | - Example: `file.inFolder("notes")` returns `true`. 443 | 444 | ## Object 445 | 446 | Functions you can use with a collection of key-value pairs such as `{"a": 1, "b": 2}`. 447 | 448 | ### `isEmpty()` 449 | 450 | `object.isEmpty(): boolean` 451 | 452 | - Returns true if the object has no own properties. 453 | - Example: `{}.isEmpty()` returns `true`. 454 | 455 | ## Regular expression 456 | 457 | Functions you can use with a regular expression pattern. Example: `/abc/`. 458 | 459 | ### `matches()` 460 | 461 | `regexp.matches(value: string): boolean` 462 | 463 | - `value` is the string to test. 464 | - Returns true if the regular expression matches `value`. 465 | - Example: `/abc/.matches("abcde")` returns `true`. -------------------------------------------------------------------------------- /src/lib/BasesUpdater.svelte: -------------------------------------------------------------------------------- 1 | 146 | 147 |
148 |
149 |

Bases Syntax Updater

150 |

Drag and drop a .base file or edit the YAML directly to update to the latest syntax

151 |
152 | 153 |
154 |
155 |
161 |

Drop your .base file here

162 |
163 | 164 | 169 | 170 | {#if error} 171 |
172 |

Error:

173 |
{error}
174 |
175 | {/if} 176 | 177 |
178 |
179 | 182 |
183 | 184 | 185 |
186 |
187 | 188 |
189 | {#if isLoading} 190 |
191 |
192 |

Updating syntax...

193 |
194 | {:else if error} 195 |
196 |

❌ Error

197 |
{error}
198 |
199 | {:else if newBasesYaml} 200 |
201 |
202 |
203 |

Updated Syntax

204 | {#if updateSummary.length > 0} 205 | {updateSummary.length} changes made 206 | {/if} 207 |
208 |
209 | 212 | 213 |
214 |
215 | 216 | 221 | 222 | {#if updateSummary.length > 0} 223 |
224 |

📋 Changes Made

225 |
    226 | {#each updateSummary as change} 227 |
  • ✅ {change}
  • 228 | {/each} 229 |
230 |
231 | {/if} 232 |
233 | {:else} 234 |
235 |

Enter old Bases YAML to see the updated syntax

236 |
237 | {/if} 238 |
239 |
240 |
241 | 242 | {#if updateSummary.length > 0 || newBasesYaml} 243 |
244 |

🔧 What gets updated?

245 |
246 |
247 |

File Functions

248 |
    249 |
  • inFolder(file.file, "folder")file.inFolder("folder")
  • 250 |
  • taggedWith(file.file, "tag")file.hasTag("tag")
  • 251 |
  • linksTo(file.file, "file")file.hasLink("file")
  • 252 |
253 |
254 | 255 |
256 |

Boolean Operators

257 |
    258 |
  • and&&
  • 259 |
  • or||
  • 260 |
  • not()!()
  • 261 |
262 |
263 | 264 |
265 |

String Operations

266 |
    267 |
  • concat(a, b, c)a + b + c
  • 268 |
  • join("", a, b)a + b
  • 269 |
  • join(" ", a, b)a + " " + b
  • 270 |
  • array.join("")array
  • 271 |
  • array.join(", ")array.join(", ")
  • 272 |
  • len()length()
  • 273 |
  • empty()isEmpty()
  • 274 |
  • notEmpty()!isEmpty()
  • 275 |
276 |
277 | 278 |
279 |

Properties & Functions

280 |
    281 |
  • file.extensionfile.ext
  • 282 |
  • dateFormat()format()
  • 283 |
  • average()average()
  • 284 |
285 |
286 | 287 |
288 |

Date & Time

289 |
    290 |
  • dateModify(date, "1 day")date + "1 day"
  • 291 |
  • duration("1 day")"1 day"
  • 292 |
  • dateDiff(a, b)a - b
  • 293 |
294 |
295 | 296 |
297 |

View Configuration

298 |
    299 |
  • Sort directions: ASC/DESCasc/desc
  • 300 |
  • Filter syntax updated to new format
  • 301 |
  • Formula expressions simplified
  • 302 |
303 |
304 |
305 |
306 | {/if} 307 | 308 | -------------------------------------------------------------------------------- /src/lib/basesUpdater.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from "js-yaml"; 2 | 3 | /** 4 | * Bases Updater - Converts old Bases syntax to new syntax 5 | */ 6 | export class BasesUpdater { 7 | /** 8 | * Convert old Bases YAML to new syntax 9 | */ 10 | updateBasesYaml(oldYaml: string): string { 11 | try { 12 | const parsed = yaml.load(oldYaml) as any; 13 | const updated = this.updateBasesObject(parsed); 14 | let result = yaml.dump(updated, { 15 | quotingType: '"', 16 | forceQuotes: false, 17 | lineWidth: -1, 18 | }); 19 | 20 | // Post-process to fix over-escaped string concatenation expressions 21 | result = this.fixEscapedStringConcatenation(result); 22 | 23 | return result; 24 | } catch (error: unknown) { 25 | const errorMessage = 26 | error instanceof Error ? error.message : "Unknown error occurred"; 27 | throw new Error(`Failed to parse YAML: ${errorMessage}`); 28 | } 29 | } 30 | 31 | /** 32 | * Update a Bases object to new syntax 33 | */ 34 | private updateBasesObject(bases: any): any { 35 | if (!bases || typeof bases !== "object") { 36 | return bases; 37 | } 38 | 39 | const updated = { ...bases }; 40 | 41 | // Update filters 42 | if (updated.filters) { 43 | updated.filters = this.updateFilters(updated.filters); 44 | } 45 | 46 | // Update formulas 47 | if (updated.formulas) { 48 | updated.formulas = this.updateFormulas(updated.formulas); 49 | } 50 | 51 | // Update views 52 | if (updated.views && Array.isArray(updated.views)) { 53 | updated.views = updated.views.map((view: any) => this.updateView(view)); 54 | } 55 | 56 | // Update display properties 57 | if (updated.display) { 58 | updated.display = this.updateDisplayProperties(updated.display); 59 | } 60 | 61 | return updated; 62 | } 63 | 64 | /** 65 | * Update filters to new syntax 66 | */ 67 | private updateFilters(filters: any): any { 68 | if (typeof filters === "string") { 69 | return this.updateFilterExpression(filters); 70 | } 71 | 72 | if (typeof filters === "object" && filters !== null) { 73 | const updated: any = {}; 74 | 75 | for (const [key, value] of Object.entries(filters)) { 76 | if (key === "and" || key === "or" || key === "not") { 77 | if (Array.isArray(value)) { 78 | updated[key] = value.map((item) => this.updateFilters(item)); 79 | } else { 80 | updated[key] = this.updateFilters(value); 81 | } 82 | } else { 83 | updated[key] = value; 84 | } 85 | } 86 | 87 | return updated; 88 | } 89 | 90 | return filters; 91 | } 92 | 93 | /** 94 | * Update a single filter expression string 95 | */ 96 | private updateFilterExpression(expression: string): string { 97 | let updated = expression; 98 | 99 | // Update function calls 100 | const functionUpdates: Record = { 101 | // File functions - convert to method style 102 | "inFolder\\(file\\.file,\\s*([^)]+)\\)": "file.inFolder($1)", 103 | "taggedWith\\(file\\.file,\\s*([^)]+)\\)": "file.hasTag($1)", 104 | "linksTo\\(file\\.file,\\s*([^)]+)\\)": "file.hasLink($1)", 105 | 106 | // Boolean operators 107 | "\\s+and\\s+": " && ", 108 | "\\s+or\\s+": " || ", 109 | "not\\(([^)]+)\\)": "!($1)", 110 | 111 | // Function name updates 112 | "\\blen\\(": "length(", 113 | "\\bcontainsAny\\(": "containsAny(", 114 | "\\bcontainsAll\\(": "containsAll(", 115 | "\\bstartswith\\(": "startsWith(", 116 | "\\bendswith\\(": "endsWith(", 117 | "\\bempty\\(": "isEmpty(", 118 | "\\bnotEmpty\\(": "!isEmpty(", 119 | "\\baverage\\(": "average(", 120 | 121 | // Date functions 122 | "\\bdateFormat\\(": "format(", 123 | "\\bdateModify\\(": "+", 124 | "\\bdateDiff\\(": "-", 125 | 126 | // Property updates 127 | "\\bfile\\.extension\\b": "file.ext", 128 | 129 | // Duration format updates 130 | 'duration\\("([^"]+)"\\)': '"$1"', 131 | }; 132 | 133 | for (const [pattern, replacement] of Object.entries(functionUpdates)) { 134 | const regex = new RegExp(pattern, "g"); 135 | updated = updated.replace(regex, replacement); 136 | } 137 | 138 | // Handle string concatenation functions 139 | updated = this.convertStringConcatenation(updated); 140 | 141 | // Handle date arithmetic - convert dateModify calls to + operator 142 | updated = updated.replace( 143 | /dateModify\(([^,]+),\s*"([^"]+)"\)/g, 144 | '$1 + "$2"' 145 | ); 146 | 147 | // Handle negated duration in date arithmetic 148 | updated = updated.replace(/\+\s*"-([^"]+)"/g, '+ "-$1"'); 149 | 150 | return updated; 151 | } 152 | 153 | /** 154 | * Update formulas to new syntax 155 | */ 156 | private updateFormulas( 157 | formulas: Record 158 | ): Record { 159 | const updated: Record = {}; 160 | 161 | for (const [key, formula] of Object.entries(formulas)) { 162 | updated[key] = this.updateFormulaExpression(formula); 163 | } 164 | 165 | return updated; 166 | } 167 | 168 | /** 169 | * Update a single formula expression 170 | */ 171 | private updateFormulaExpression(formula: string): string { 172 | let updated = formula; 173 | 174 | // Update function calls in formulas 175 | const formulaUpdates: Record = { 176 | // Method calls 177 | "\\.toFixed\\(": ".toFixed(", 178 | "\\.toString\\(": ".toString(", 179 | "\\.trim\\(": ".trim(", 180 | "\\.title\\(": ".title(", 181 | "\\.split\\(": ".split(", 182 | "\\.replace\\(": ".replace(", 183 | "\\.slice\\(": ".slice(", 184 | "\\.reverse\\(": ".reverse(", 185 | "\\.sort\\(": ".sort(", 186 | "\\.flat\\(": ".flat(", 187 | "\\.unique\\(": ".unique(", 188 | "\\.contains\\(": ".contains(", 189 | "\\.containsAny\\(": ".containsAny(", 190 | "\\.containsAll\\(": ".containsAll(", 191 | "\\.startsWith\\(": ".startsWith(", 192 | "\\.endsWith\\(": ".endsWith(", 193 | "\\.isEmpty\\(": ".isEmpty(", 194 | 195 | // Math functions 196 | "\\.round\\(": ".round(", 197 | "\\.floor\\(": ".floor(", 198 | "\\.ceil\\(": ".ceil(", 199 | "\\.abs\\(": ".abs(", 200 | 201 | // Date properties 202 | "\\.year\\b": ".year", 203 | "\\.month\\b": ".month", 204 | "\\.day\\b": ".day", 205 | "\\.hour\\b": ".hour", 206 | "\\.minute\\b": ".minute", 207 | "\\.second\\b": ".second", 208 | "\\.millisecond\\b": ".millisecond", 209 | 210 | // Date methods 211 | "\\.format\\(": ".format(", 212 | "\\.date\\(": ".date(", 213 | "\\.time\\(": ".time(", 214 | 215 | // Array/String methods 216 | "\\.length\\b": ".length", 217 | 218 | // Boolean operators in formulas 219 | "\\s+and\\s+": " && ", 220 | "\\s+or\\s+": " || ", 221 | 222 | // Function name updates 223 | "\\blen\\(": "length(", 224 | "\\baverage\\(": "average(", 225 | }; 226 | 227 | for (const [pattern, replacement] of Object.entries(formulaUpdates)) { 228 | const regex = new RegExp(pattern, "g"); 229 | updated = updated.replace(regex, replacement); 230 | } 231 | 232 | // Handle string concatenation functions 233 | updated = this.convertStringConcatenation(updated); 234 | 235 | return updated; 236 | } 237 | 238 | /** 239 | * Update view configuration 240 | */ 241 | private updateView(view: any): any { 242 | if (!view || typeof view !== "object") { 243 | return view; 244 | } 245 | 246 | const updated = { ...view }; 247 | 248 | // Update filters in view 249 | if (updated.filters) { 250 | updated.filters = this.updateFilters(updated.filters); 251 | } 252 | 253 | // Update sort configuration 254 | if (updated.sort && Array.isArray(updated.sort)) { 255 | updated.sort = updated.sort.map((sortItem: any) => { 256 | if (typeof sortItem === "object" && sortItem.direction) { 257 | return { 258 | ...sortItem, 259 | direction: sortItem.direction.toLowerCase(), // ASC/DESC -> asc/desc 260 | }; 261 | } 262 | return sortItem; 263 | }); 264 | } 265 | 266 | return updated; 267 | } 268 | 269 | /** 270 | * Update display property names 271 | */ 272 | private updateDisplayProperties( 273 | display: Record 274 | ): Record { 275 | const updated: Record = {}; 276 | 277 | for (const [key, value] of Object.entries(display)) { 278 | let updatedKey = key; 279 | 280 | // Update property names 281 | if (key === "file.extension") { 282 | updatedKey = "file.ext"; 283 | } 284 | 285 | updated[updatedKey] = value; 286 | } 287 | 288 | return updated; 289 | } 290 | 291 | /** 292 | * Get a summary of changes made 293 | */ 294 | getUpdateSummary(oldYaml: string, newYaml: string): string[] { 295 | const changes: string[] = []; 296 | 297 | // Check for specific patterns that were updated 298 | const patterns = [ 299 | { 300 | old: /inFolder\(file\.file,/, 301 | new: /file\.inFolder\(/, 302 | desc: "Updated inFolder() to file.inFolder()", 303 | }, 304 | { 305 | old: /taggedWith\(file\.file,/, 306 | new: /file\.hasTag\(/, 307 | desc: "Updated taggedWith() to file.hasTag()", 308 | }, 309 | { 310 | old: /linksTo\(file\.file,/, 311 | new: /file\.hasLink\(/, 312 | desc: "Updated linksTo() to file.hasLink()", 313 | }, 314 | { old: /\sand\s/, new: /&&/, desc: 'Updated "and" to "&&"' }, 315 | { old: /\sor\s/, new: /\|\|/, desc: 'Updated "or" to "||"' }, 316 | { old: /not\(/, new: /!\(/, desc: "Updated not() to !()" }, 317 | { 318 | old: /file\.extension/, 319 | new: /file\.ext/, 320 | desc: "Updated file.extension to file.ext", 321 | }, 322 | { 323 | old: /dateModify\(/, 324 | new: /\+/, 325 | desc: "Updated dateModify() to + operator", 326 | }, 327 | { 328 | old: /ASC|DESC/, 329 | new: /asc|desc/, 330 | desc: "Updated sort direction to lowercase", 331 | }, 332 | { 333 | old: /concat\(/, 334 | new: /\+/, 335 | desc: "Updated concat() to + operator for string concatenation", 336 | }, 337 | { 338 | old: /join\("",/, 339 | new: /\+/, 340 | desc: 'Updated join("", ...) to + operator for string concatenation', 341 | }, 342 | { 343 | old: /join\("[^"]*",/, 344 | new: /\+/, 345 | desc: "Updated join() with separator to + operator", 346 | }, 347 | { 348 | old: /\.join\(""/, 349 | new: /\+/, 350 | desc: 'Updated .join("") to + operator for string concatenation', 351 | }, 352 | { 353 | old: /duration\("/, 354 | new: /"/, 355 | desc: "Updated duration() function to string format", 356 | }, 357 | { old: /len\(/, new: /length\(/, desc: "Updated len() to length()" }, 358 | { 359 | old: /empty\(/, 360 | new: /isEmpty\(/, 361 | desc: "Updated empty() to isEmpty()", 362 | }, 363 | { 364 | old: /notEmpty\(/, 365 | new: /!isEmpty\(/, 366 | desc: "Updated notEmpty() to !isEmpty()", 367 | }, 368 | ]; 369 | 370 | for (const pattern of patterns) { 371 | if (pattern.old.test(oldYaml) && pattern.new.test(newYaml)) { 372 | changes.push(pattern.desc); 373 | } 374 | } 375 | 376 | return changes; 377 | } 378 | 379 | /** 380 | * Convert string concatenation functions to + operator 381 | */ 382 | private convertStringConcatenation(expression: string): string { 383 | let updated = expression; 384 | let previousUpdate = ""; 385 | 386 | // Keep applying transformations until no more changes are made 387 | while (updated !== previousUpdate) { 388 | previousUpdate = updated; 389 | 390 | // Handle concat() function calls 391 | updated = this.replaceFunctionCalls(updated, "concat", (args) => { 392 | const argList = this.parseArguments(args); 393 | return argList.join(" + "); 394 | }); 395 | 396 | // Handle join() function calls with empty string separator 397 | updated = this.replaceFunctionCalls(updated, "join", (args) => { 398 | const argList = this.parseArguments(args); 399 | if ( 400 | argList.length > 0 && 401 | (argList[0] === '""' || argList[0] === "''") 402 | ) { 403 | // Remove the empty separator and join the rest 404 | return argList.slice(1).join(" + "); 405 | } 406 | // Handle join with separator 407 | if (argList.length > 1) { 408 | const separator = argList[0]; 409 | const cleanSeparator = separator.slice(1, -1); // Remove quotes 410 | if (cleanSeparator === "") { 411 | return argList.slice(1).join(" + "); 412 | } else { 413 | return argList.slice(1).join(` + ${separator} + `); 414 | } 415 | } 416 | return args; // Return original if can't parse 417 | }); 418 | } 419 | 420 | // Handle .join() method calls on arrays 421 | updated = updated.replace( 422 | /(\w+)\.join\s*\(\s*"([^"]*)"\s*\)/g, 423 | (match, arrayName, separator) => { 424 | if (separator === "") { 425 | return arrayName; // Direct concatenation for empty separator 426 | } else { 427 | return `${arrayName}.join("${separator}")`; 428 | } 429 | } 430 | ); 431 | 432 | return updated; 433 | } 434 | 435 | /** 436 | * Replace function calls with a transformation function 437 | */ 438 | private replaceFunctionCalls( 439 | text: string, 440 | functionName: string, 441 | transform: (args: string) => string 442 | ): string { 443 | const regex = new RegExp(`\\b${functionName}\\s*\\(`, "g"); 444 | let result = ""; 445 | let lastIndex = 0; 446 | let match; 447 | 448 | while ((match = regex.exec(text)) !== null) { 449 | const startIndex = match.index; 450 | const openParenIndex = match.index + match[0].length - 1; 451 | 452 | // Add text before the function call 453 | result += text.substring(lastIndex, startIndex); 454 | 455 | // Find the matching closing parenthesis 456 | const args = this.extractArgumentsFromPosition(text, openParenIndex); 457 | if (args !== null) { 458 | // Apply the transformation 459 | const transformed = transform(args); 460 | result += transformed; 461 | lastIndex = openParenIndex + args.length + 2; // +2 for the parentheses 462 | } else { 463 | // If we can't parse it, keep the original 464 | result += match[0]; 465 | lastIndex = regex.lastIndex; 466 | } 467 | } 468 | 469 | // Add remaining text 470 | result += text.substring(lastIndex); 471 | return result; 472 | } 473 | 474 | /** 475 | * Extract arguments from a function call starting at the opening parenthesis 476 | */ 477 | private extractArgumentsFromPosition( 478 | text: string, 479 | openParenIndex: number 480 | ): string | null { 481 | let depth = 0; 482 | let inQuotes = false; 483 | let quoteChar = ""; 484 | let i = openParenIndex; 485 | 486 | while (i < text.length) { 487 | const char = text[i]; 488 | const prevChar = i > 0 ? text[i - 1] : ""; 489 | 490 | if ((char === '"' || char === "'") && prevChar !== "\\") { 491 | if (!inQuotes) { 492 | inQuotes = true; 493 | quoteChar = char; 494 | } else if (char === quoteChar) { 495 | inQuotes = false; 496 | quoteChar = ""; 497 | } 498 | } 499 | 500 | if (!inQuotes) { 501 | if (char === "(") { 502 | depth++; 503 | } else if (char === ")") { 504 | depth--; 505 | if (depth === 0) { 506 | // Found the matching closing parenthesis 507 | return text.substring(openParenIndex + 1, i); 508 | } 509 | } 510 | } 511 | 512 | i++; 513 | } 514 | 515 | return null; 516 | } 517 | 518 | /** 519 | * Extract a complete function call with proper parentheses matching 520 | */ 521 | private extractFunctionCall( 522 | text: string, 523 | startOffset: number, 524 | functionName: string 525 | ): { args: string } | null { 526 | // Find the opening parenthesis 527 | const openParenIndex = text.indexOf("(", startOffset); 528 | if (openParenIndex === -1) return null; 529 | 530 | const args = this.extractArgumentsFromPosition(text, openParenIndex); 531 | return args !== null ? { args } : null; 532 | } 533 | 534 | /** 535 | * Parse function arguments, handling nested parentheses and quotes 536 | */ 537 | private parseArguments(argsString: string): string[] { 538 | const args: string[] = []; 539 | let current = ""; 540 | let depth = 0; 541 | let inQuotes = false; 542 | let quoteChar = ""; 543 | 544 | for (let i = 0; i < argsString.length; i++) { 545 | const char = argsString[i]; 546 | const prevChar = i > 0 ? argsString[i - 1] : ""; 547 | 548 | if ((char === '"' || char === "'") && prevChar !== "\\") { 549 | if (!inQuotes) { 550 | inQuotes = true; 551 | quoteChar = char; 552 | } else if (char === quoteChar) { 553 | inQuotes = false; 554 | quoteChar = ""; 555 | } 556 | } 557 | 558 | if (!inQuotes) { 559 | if (char === "(") { 560 | depth++; 561 | } else if (char === ")") { 562 | depth--; 563 | } else if (char === "," && depth === 0) { 564 | args.push(current.trim()); 565 | current = ""; 566 | continue; 567 | } 568 | } 569 | 570 | current += char; 571 | } 572 | 573 | if (current.trim()) { 574 | args.push(current.trim()); 575 | } 576 | 577 | return args; 578 | } 579 | 580 | /** 581 | * Fix over-escaped string concatenation expressions in YAML output 582 | */ 583 | private fixEscapedStringConcatenation(yamlString: string): string { 584 | // Fix expressions that contain string concatenation with + operator 585 | // Pattern: key: "\"string\" + variable + \"string\"" 586 | // Should become: key: "string" + variable + "string" 587 | return yamlString.replace( 588 | /^(\s*)([^:\s]+):\s*"((?:[^"\\]|\\.)*)"\s*$/gm, 589 | (match, indent, key, expression) => { 590 | // Check if this looks like a string concatenation expression with escaped quotes 591 | if (expression.includes(' + ') && expression.includes('\\"')) { 592 | // Unescape the quotes in the expression and remove the outer quotes 593 | const unescaped = expression.replace(/\\"/g, '"'); 594 | return `${indent}${key}: ${unescaped}`; 595 | } 596 | return match; // Return original if it doesn't match our pattern 597 | } 598 | ); 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /src/dataview-parser/expression-parse.ts: -------------------------------------------------------------------------------- 1 | import type { Literal } from "./field"; 2 | import * as P from "parsimmon"; 3 | import type { 4 | BinaryOp, 5 | Field, 6 | LambdaField, 7 | ListField, 8 | LiteralField, 9 | ObjectField, 10 | VariableField, 11 | } from "./field"; 12 | import { Fields } from "./field"; 13 | import type { 14 | FolderSource, 15 | NegatedSource, 16 | Source, 17 | SourceOp, 18 | TagSource, 19 | CsvSource, 20 | } from "./source-types"; 21 | import { DateTime, Duration } from "luxon"; 22 | import { Sources } from "./source-types"; 23 | import { Result } from "./query-types"; 24 | import emojiRegex from "emoji-regex"; 25 | import { Link } from "./normalize"; 26 | 27 | /** Emoji regex without any additional flags. */ 28 | const EMOJI_REGEX = new RegExp(emojiRegex(), ""); 29 | 30 | /** Normalize a duration to all of the proper units. */ 31 | export function normalizeDuration(dur: Duration) { 32 | if (dur === undefined || dur === null) return dur; 33 | 34 | return dur.shiftToAll().normalize(); 35 | } 36 | 37 | /** Provides a lookup table for unit durations of the given type. */ 38 | export const DURATION_TYPES = { 39 | year: Duration.fromObject({ years: 1 }), 40 | years: Duration.fromObject({ years: 1 }), 41 | yr: Duration.fromObject({ years: 1 }), 42 | yrs: Duration.fromObject({ years: 1 }), 43 | 44 | month: Duration.fromObject({ months: 1 }), 45 | months: Duration.fromObject({ months: 1 }), 46 | mo: Duration.fromObject({ months: 1 }), 47 | mos: Duration.fromObject({ months: 1 }), 48 | 49 | week: Duration.fromObject({ weeks: 1 }), 50 | weeks: Duration.fromObject({ weeks: 1 }), 51 | wk: Duration.fromObject({ weeks: 1 }), 52 | wks: Duration.fromObject({ weeks: 1 }), 53 | w: Duration.fromObject({ weeks: 1 }), 54 | 55 | day: Duration.fromObject({ days: 1 }), 56 | days: Duration.fromObject({ days: 1 }), 57 | d: Duration.fromObject({ days: 1 }), 58 | 59 | hour: Duration.fromObject({ hours: 1 }), 60 | hours: Duration.fromObject({ hours: 1 }), 61 | hr: Duration.fromObject({ hours: 1 }), 62 | hrs: Duration.fromObject({ hours: 1 }), 63 | h: Duration.fromObject({ hours: 1 }), 64 | 65 | minute: Duration.fromObject({ minutes: 1 }), 66 | minutes: Duration.fromObject({ minutes: 1 }), 67 | min: Duration.fromObject({ minutes: 1 }), 68 | mins: Duration.fromObject({ minutes: 1 }), 69 | m: Duration.fromObject({ minutes: 1 }), 70 | 71 | second: Duration.fromObject({ seconds: 1 }), 72 | seconds: Duration.fromObject({ seconds: 1 }), 73 | sec: Duration.fromObject({ seconds: 1 }), 74 | secs: Duration.fromObject({ seconds: 1 }), 75 | s: Duration.fromObject({ seconds: 1 }), 76 | }; 77 | 78 | /** Shorthand for common dates (relative to right now). */ 79 | export const DATE_SHORTHANDS = { 80 | now: () => "now()", 81 | today: () => "now()", 82 | yesterday: () => "dateModify(date(now()), -1 day)", 83 | tomorrow: () => "dateModify(date(now()), 1 day)", 84 | sow: () => "dateModify(date(now()), -day(now()) + 1 day)", 85 | "start-of-week": () => "dateModify(date(now()), -day(now()) + 1 day)", 86 | eow: () => "dateModify(date(now()), 7 - day(now()) day)", 87 | "end-of-week": () => "dateModify(date(now()), 7 - day(now()) day)", 88 | soy: () => "date(year(now()) + '-01-01')", 89 | "start-of-year": () => "date(year(now()) + '-01-01')", 90 | eoy: () => "date(year(now()) + '-12-31')", 91 | "end-of-year": () => "date(year(now()) + '-12-31')", 92 | som: () => "date(year(now()) + '-' + month(now()) + '-01')", 93 | "start-of-month": () => "date(year(now()) + '-' + month(now()) + '-01')", 94 | eom: () => 95 | "dateModify(date(year(now()) + '-' + month(now()) + '-01'), 1 month, -1 day)", 96 | "end-of-month": () => 97 | "dateModify(date(year(now()) + '-' + month(now()) + '-01'), 1 month, -1 day)", 98 | }; 99 | 100 | /** 101 | * Keywords which cannot be used as variables directly. Use `row.` if it is a variable you have defined and want 102 | * to access. 103 | */ 104 | export const KEYWORDS = ["FROM", "WHERE", "LIMIT", "GROUP", "FLATTEN"]; 105 | 106 | /////////////// 107 | // Utilities // 108 | /////////////// 109 | 110 | /** Split on unescaped pipes in an inner link. */ 111 | function splitOnUnescapedPipe(link: string): [string, string | undefined] { 112 | let pipe = -1; 113 | while ((pipe = link.indexOf("|", pipe + 1)) >= 0) { 114 | if (pipe > 0 && link[pipe - 1] == "\\") continue; 115 | return [ 116 | link.substring(0, pipe).replace(/\\\|/g, "|"), 117 | link.substring(pipe + 1), 118 | ]; 119 | } 120 | 121 | return [link.replace(/\\\|/g, "|"), undefined]; 122 | } 123 | 124 | /** Attempt to parse the inside of a link to pull out display name, subpath, etc. */ 125 | export function parseInnerLink(rawlink: string): Link { 126 | let [link, display] = splitOnUnescapedPipe(rawlink); 127 | return Link.infer(link, false, display); 128 | } 129 | 130 | /** Create a left-associative binary parser which parses the given sub-element and separator. Handles whitespace. */ 131 | export function createBinaryParser( 132 | child: P.Parser, 133 | sep: P.Parser, 134 | combine: (a: T, b: U, c: T) => T 135 | ): P.Parser { 136 | return P.seqMap( 137 | child, 138 | P.seq(P.optWhitespace, sep, P.optWhitespace, child).many(), 139 | (first, rest) => { 140 | if (rest.length == 0) return first; 141 | 142 | let node = combine(first, rest[0][1], rest[0][3]); 143 | for (let index = 1; index < rest.length; index++) { 144 | node = combine(node, rest[index][1], rest[index][3]); 145 | } 146 | return node; 147 | } 148 | ); 149 | } 150 | 151 | export function chainOpt( 152 | base: P.Parser, 153 | ...funcs: ((r: T) => P.Parser)[] 154 | ): P.Parser { 155 | return P.custom((success, failure) => { 156 | return (input, i) => { 157 | let result = (base as any)._(input, i); 158 | if (!result.status) return result; 159 | 160 | for (let func of funcs) { 161 | let next = (func(result.value as T) as any)._(input, result.index); 162 | if (!next.status) return result; 163 | 164 | result = next; 165 | } 166 | 167 | return result; 168 | }; 169 | }); 170 | } 171 | 172 | //////////////////////// 173 | // Expression Parsing // 174 | //////////////////////// 175 | 176 | export type PostfixFragment = 177 | | { type: "dot"; field: string } 178 | | { type: "index"; field: Field } 179 | | { type: "function"; fields: Field[] }; 180 | 181 | export interface ExpressionLanguage { 182 | number: number; 183 | string: string; 184 | escapeCharacter: string; 185 | bool: boolean; 186 | tag: string; 187 | identifier: string; 188 | link: Link; 189 | embedLink: Link; 190 | rootDate: DateTime; 191 | dateShorthand: keyof typeof DATE_SHORTHANDS; 192 | date: DateTime; 193 | datePlus: DateTime; 194 | durationType: keyof typeof DURATION_TYPES; 195 | duration: Duration; 196 | rawNull: string; 197 | 198 | binaryPlusMinus: BinaryOp; 199 | binaryMulDiv: BinaryOp; 200 | binaryCompareOp: BinaryOp; 201 | binaryBooleanOp: BinaryOp; 202 | 203 | // Source-related parsers. 204 | tagSource: TagSource; 205 | csvSource: CsvSource; 206 | folderSource: FolderSource; 207 | parensSource: Source; 208 | atomSource: Source; 209 | linkIncomingSource: Source; 210 | linkOutgoingSource: Source; 211 | negateSource: NegatedSource; 212 | binaryOpSource: Source; 213 | source: Source; 214 | 215 | // Field-related parsers. 216 | variableField: VariableField; 217 | numberField: LiteralField; 218 | boolField: LiteralField; 219 | stringField: LiteralField; 220 | dateField: LiteralField; 221 | durationField: LiteralField; 222 | linkField: LiteralField; 223 | nullField: LiteralField; 224 | 225 | listField: ListField; 226 | objectField: ObjectField; 227 | 228 | atomInlineField: Literal; 229 | inlineFieldList: Literal[]; 230 | inlineField: Literal; 231 | 232 | negatedField: Field; 233 | atomField: Field; 234 | indexField: Field; 235 | lambdaField: LambdaField; 236 | 237 | // Postfix parsers for function calls & the like. 238 | dotPostfix: PostfixFragment; 239 | indexPostfix: PostfixFragment; 240 | functionPostfix: PostfixFragment; 241 | 242 | // Binary op parsers. 243 | binaryMulDivField: Field; 244 | binaryPlusMinusField: Field; 245 | binaryCompareField: Field; 246 | binaryBooleanField: Field; 247 | binaryOpField: Field; 248 | parensField: Field; 249 | field: Field; 250 | } 251 | 252 | export const EXPRESSION = P.createLanguage({ 253 | // A floating point number; the decimal point is optional. 254 | number: (q) => 255 | P.regexp(/-?[0-9]+(\.[0-9]+)?/) 256 | .map((str) => Number.parseFloat(str)) 257 | .desc("number"), 258 | 259 | // A quote-surrounded string which supports escape characters ('\'). 260 | string: (q) => 261 | P.string('"') 262 | .then( 263 | P.alt(q.escapeCharacter, P.noneOf('"\\')) 264 | .atLeast(0) 265 | .map((chars) => chars.join("")) 266 | ) 267 | .skip(P.string('"')) 268 | .desc("string"), 269 | 270 | escapeCharacter: (_) => 271 | P.string("\\") 272 | .then(P.any) 273 | .map((escaped) => { 274 | // If we are escaping a backslash or a quote, pass in on in escaped form 275 | if (escaped === '"') return '"'; 276 | if (escaped === "\\") return "\\"; 277 | else return "\\" + escaped; 278 | }), 279 | 280 | // A boolean true/false value. 281 | bool: (_) => 282 | P.regexp(/true|false|True|False/) 283 | .map((str) => str.toLowerCase() == "true") 284 | .desc("boolean ('true' or 'false')"), 285 | 286 | // A tag of the form '#stuff/hello-there'. 287 | tag: (_) => 288 | P.seqMap( 289 | P.string("#"), 290 | P.alt( 291 | P.regexp( 292 | /[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~\[\]\\\s]/ 293 | ).desc("text") 294 | ).many(), 295 | (start, rest) => start + rest.join("") 296 | ).desc("tag ('#hello/stuff')"), 297 | 298 | // A variable identifier, which is alphanumeric and must start with a letter or... emoji. 299 | identifier: (_) => 300 | P.seqMap( 301 | P.alt(P.regexp(/\p{Letter}/u), P.regexp(EMOJI_REGEX).desc("text")), 302 | P.alt( 303 | P.regexp(/[0-9\p{Letter}_-]/u), 304 | P.regexp(EMOJI_REGEX).desc("text") 305 | ).many(), 306 | (first, rest) => first + rest.join("") 307 | ).desc("variable identifier"), 308 | 309 | // An Obsidian link of the form [[]]. 310 | link: (_) => 311 | P.regexp(/\[\[([^\[\]]*?)\]\]/u, 1) 312 | .map((linkInner) => parseInnerLink(linkInner)) 313 | .desc("file link"), 314 | 315 | // An embeddable link which can start with '!'. This overlaps with the normal negation operator, so it is only 316 | // provided for metadata parsing. 317 | embedLink: (q) => 318 | P.seqMap(P.string("!").atMost(1), q.link, (p, l) => { 319 | if (p.length > 0) l.embed = true; 320 | return l; 321 | }).desc("file link"), 322 | 323 | // Binary plus or minus operator. 324 | binaryPlusMinus: (_) => 325 | P.regexp(/\+|-/) 326 | .map((str) => str as BinaryOp) 327 | .desc("'+' or '-'"), 328 | 329 | // Binary times or divide operator. 330 | binaryMulDiv: (_) => 331 | P.regexp(/\*|\/|%/) 332 | .map((str) => str as BinaryOp) 333 | .desc("'*' or '/' or '%'"), 334 | 335 | // Binary comparison operator. 336 | binaryCompareOp: (_) => 337 | P.regexp(/>=|<=|!=|>|<|=/) 338 | .map((str) => str as BinaryOp) 339 | .desc("'>=' or '<=' or '!=' or '=' or '>' or '<'"), 340 | 341 | // Binary boolean combination operator. 342 | binaryBooleanOp: (_) => 343 | P.regexp(/and|or|&|\|/i) 344 | .map((str) => { 345 | if (str.toLowerCase() == "and") return "&"; 346 | else if (str.toLowerCase() == "or") return "|"; 347 | else return str as BinaryOp; 348 | }) 349 | .desc("'and' or 'or'"), 350 | 351 | // A date which can be YYYY-MM[-DDTHH:mm:ss]. 352 | rootDate: (_) => 353 | P.seqMap( 354 | P.regexp(/\d{4}/), 355 | P.string("-"), 356 | P.regexp(/\d{2}/), 357 | (year, _, month) => { 358 | return DateTime.fromObject({ 359 | year: Number.parseInt(year), 360 | month: Number.parseInt(month), 361 | }); 362 | } 363 | ).desc("date in format YYYY-MM[-DDTHH-MM-SS.MS]"), 364 | dateShorthand: (_) => 365 | P.alt( 366 | ...Object.keys(DATE_SHORTHANDS) 367 | .sort((a, b) => b.length - a.length) 368 | .map(P.string) 369 | ) as P.Parser, 370 | date: (q) => 371 | chainOpt( 372 | q.rootDate, 373 | (ym: DateTime) => 374 | P.seqMap(P.string("-"), P.regexp(/\d{2}/), (_, day) => 375 | ym.set({ day: Number.parseInt(day) }) 376 | ), 377 | (ymd: DateTime) => 378 | P.seqMap(P.string("T"), P.regexp(/\d{2}/), (_, hour) => 379 | ymd.set({ hour: Number.parseInt(hour) }) 380 | ), 381 | (ymdh: DateTime) => 382 | P.seqMap(P.string(":"), P.regexp(/\d{2}/), (_, minute) => 383 | ymdh.set({ minute: Number.parseInt(minute) }) 384 | ), 385 | (ymdhm: DateTime) => 386 | P.seqMap(P.string(":"), P.regexp(/\d{2}/), (_, second) => 387 | ymdhm.set({ second: Number.parseInt(second) }) 388 | ), 389 | (ymdhms: DateTime) => 390 | P.alt( 391 | P.seqMap(P.string("."), P.regexp(/\d{3}/), (_, millisecond) => 392 | ymdhms.set({ millisecond: Number.parseInt(millisecond) }) 393 | ), 394 | P.succeed(ymdhms) // pass 395 | ), 396 | (dt: DateTime) => 397 | P.alt( 398 | P.seqMap( 399 | P.string("+").or(P.string("-")), 400 | P.regexp(/\d{1,2}(:\d{2})?/), 401 | (pm, hr) => dt.setZone("UTC" + pm + hr, { keepLocalTime: true }) 402 | ), 403 | P.seqMap(P.string("Z"), () => 404 | dt.setZone("utc", { keepLocalTime: true }) 405 | ), 406 | P.seqMap( 407 | P.string("["), 408 | P.regexp(/[0-9A-Za-z+-\/]+/u), 409 | P.string("]"), 410 | (_a, zone, _b) => dt.setZone(zone, { keepLocalTime: true }) 411 | ) 412 | ) 413 | ) 414 | .assert((dt: DateTime) => dt.isValid, "valid date") 415 | .desc("date in format YYYY-MM[-DDTHH-MM-SS.MS]"), 416 | 417 | // A date, plus various shorthand times of day it could be. 418 | datePlus: (q) => q.date, 419 | 420 | // A duration of time. 421 | durationType: (_) => 422 | P.alt( 423 | ...Object.keys(DURATION_TYPES) 424 | .sort((a, b) => b.length - a.length) 425 | .map(P.string) 426 | ) as P.Parser, 427 | duration: (q) => 428 | P.seqMap(q.number, P.optWhitespace, q.durationType, (count, _, t) => 429 | DURATION_TYPES[t].mapUnits((x) => x * count) 430 | ) 431 | .sepBy1(P.string(",").trim(P.optWhitespace).or(P.optWhitespace)) 432 | .map((durations) => durations.reduce((p, c) => p.plus(c))) 433 | .desc("duration like 4hr2min"), 434 | 435 | // A raw null value. 436 | rawNull: (_) => P.string("null"), 437 | 438 | // Source parsing. 439 | tagSource: (q) => q.tag.map((tag) => Sources.tag(tag)), 440 | csvSource: (q) => 441 | P.seqMap( 442 | P.string("csv(").skip(P.optWhitespace), 443 | q.string, 444 | P.string(")"), 445 | (_1, path, _2) => Sources.csv(path) 446 | ), 447 | linkIncomingSource: (q) => 448 | q.link.map((link) => Sources.link(link.path, true)), 449 | linkOutgoingSource: (q) => 450 | P.seqMap( 451 | P.string("outgoing(").skip(P.optWhitespace), 452 | q.link, 453 | P.string(")"), 454 | (_1, link, _2) => Sources.link(link.path, false) 455 | ), 456 | folderSource: (q) => q.string.map((str) => Sources.folder(str)), 457 | parensSource: (q) => 458 | P.seqMap( 459 | P.string("("), 460 | P.optWhitespace, 461 | q.source, 462 | P.optWhitespace, 463 | P.string(")"), 464 | (_1, _2, field, _3, _4) => field 465 | ), 466 | negateSource: (q) => 467 | P.seqMap(P.alt(P.string("-"), P.string("!")), q.atomSource, (_, source) => 468 | Sources.negate(source) 469 | ), 470 | atomSource: (q) => 471 | P.alt( 472 | q.parensSource, 473 | q.negateSource, 474 | q.linkOutgoingSource, 475 | q.linkIncomingSource, 476 | q.folderSource, 477 | q.tagSource, 478 | q.csvSource 479 | ), 480 | binaryOpSource: (q) => 481 | createBinaryParser( 482 | q.atomSource, 483 | q.binaryBooleanOp.map((s) => s as SourceOp), 484 | Sources.binaryOp 485 | ), 486 | source: (q) => q.binaryOpSource, 487 | 488 | // Field parsing. 489 | variableField: (q) => 490 | q.identifier 491 | .chain((r) => { 492 | if (KEYWORDS.includes(r.toUpperCase())) { 493 | return P.fail( 494 | "Variable fields cannot be a keyword (" + 495 | KEYWORDS.join(" or ") + 496 | ")" 497 | ); 498 | } else { 499 | return P.succeed(Fields.variable(r)); 500 | } 501 | }) 502 | .desc("variable"), 503 | numberField: (q) => q.number.map((val) => Fields.literal(val)).desc("number"), 504 | stringField: (q) => q.string.map((val) => Fields.literal(val)).desc("string"), 505 | boolField: (q) => q.bool.map((val) => Fields.literal(val)).desc("boolean"), 506 | dateField: (q) => 507 | P.seqMap( 508 | P.string("date("), 509 | P.optWhitespace, 510 | q.datePlus, 511 | P.optWhitespace, 512 | P.string(")"), 513 | (prefix, _1, date, _2, postfix) => Fields.literal(date) 514 | ).desc("date"), 515 | durationField: (q) => 516 | P.seqMap( 517 | P.string("dur("), 518 | P.optWhitespace, 519 | q.duration, 520 | P.optWhitespace, 521 | P.string(")"), 522 | (prefix, _1, dur, _2, postfix) => Fields.literal(dur) 523 | ).desc("duration"), 524 | nullField: (q) => q.rawNull.map((_) => Fields.NULL), 525 | linkField: (q) => q.link.map((f) => Fields.literal(f)), 526 | listField: (q) => 527 | q.field 528 | .sepBy(P.string(",").trim(P.optWhitespace)) 529 | .wrap( 530 | P.string("[").skip(P.optWhitespace), 531 | P.optWhitespace.then(P.string("]")) 532 | ) 533 | .map((l) => Fields.list(l)) 534 | .desc("list ('[1, 2, 3]')"), 535 | objectField: (q) => 536 | P.seqMap( 537 | q.identifier.or(q.string), 538 | P.string(":").trim(P.optWhitespace), 539 | q.field, 540 | (name, _sep, value) => { 541 | return { name, value }; 542 | } 543 | ) 544 | .sepBy(P.string(",").trim(P.optWhitespace)) 545 | .wrap( 546 | P.string("{").skip(P.optWhitespace), 547 | P.optWhitespace.then(P.string("}")) 548 | ) 549 | .map((vals) => { 550 | let res: Record = {}; 551 | for (let entry of vals) res[entry.name] = entry.value; 552 | return Fields.object(res); 553 | }) 554 | .desc("object ('{ a: 1, b: 2 }')"), 555 | 556 | atomInlineField: (q) => 557 | P.alt( 558 | q.date, 559 | q.duration.map((d) => normalizeDuration(d)), 560 | q.string, 561 | q.tag, 562 | q.embedLink, 563 | q.bool, 564 | q.number, 565 | q.rawNull 566 | ), 567 | inlineFieldList: (q) => 568 | q.atomInlineField.sepBy( 569 | P.string(",").trim(P.optWhitespace).lookahead(q.atomInlineField) 570 | ), 571 | inlineField: (q) => 572 | P.alt( 573 | P.seqMap( 574 | q.atomInlineField, 575 | P.string(",").trim(P.optWhitespace), 576 | q.inlineFieldList, 577 | (f, _s, l) => [f].concat(l) 578 | ), 579 | q.atomInlineField 580 | ), 581 | 582 | atomField: (q) => 583 | P.alt( 584 | // Place embed links above negated fields as they are the special parser case '![[thing]]' and are generally unambiguous. 585 | q.embedLink.map((l) => Fields.literal(l)), 586 | q.negatedField, 587 | q.linkField, 588 | q.listField, 589 | q.objectField, 590 | q.lambdaField, 591 | q.parensField, 592 | q.boolField, 593 | q.numberField, 594 | q.stringField, 595 | q.dateField, 596 | q.durationField, 597 | q.nullField, 598 | q.variableField 599 | ), 600 | indexField: (q) => 601 | P.seqMap( 602 | q.atomField, 603 | P.alt(q.dotPostfix, q.indexPostfix, q.functionPostfix).many(), 604 | (obj, postfixes) => { 605 | let result = obj; 606 | for (let post of postfixes) { 607 | switch (post.type) { 608 | case "dot": 609 | result = Fields.index(result, Fields.literal(post.field)); 610 | break; 611 | case "index": 612 | result = Fields.index(result, post.field); 613 | break; 614 | case "function": 615 | result = Fields.func(result, post.fields); 616 | break; 617 | } 618 | } 619 | 620 | return result; 621 | } 622 | ), 623 | negatedField: (q) => 624 | P.seqMap(P.string("!"), q.indexField, (_, field) => 625 | Fields.negate(field) 626 | ).desc("negated field"), 627 | parensField: (q) => 628 | P.seqMap( 629 | P.string("("), 630 | P.optWhitespace, 631 | q.field, 632 | P.optWhitespace, 633 | P.string(")"), 634 | (_1, _2, field, _3, _4) => field 635 | ), 636 | lambdaField: (q) => 637 | P.seqMap( 638 | q.identifier 639 | .sepBy(P.string(",").trim(P.optWhitespace)) 640 | .wrap( 641 | P.string("(").trim(P.optWhitespace), 642 | P.string(")").trim(P.optWhitespace) 643 | ), 644 | P.string("=>").trim(P.optWhitespace), 645 | q.field, 646 | (ident, _ignore, value) => { 647 | return { type: "lambda", arguments: ident, value }; 648 | } 649 | ), 650 | 651 | dotPostfix: (q) => 652 | P.seqMap(P.string("."), q.identifier, (_, field) => { 653 | return { type: "dot", field: field }; 654 | }), 655 | indexPostfix: (q) => 656 | P.seqMap( 657 | P.string("["), 658 | P.optWhitespace, 659 | q.field, 660 | P.optWhitespace, 661 | P.string("]"), 662 | (_, _2, field, _3, _4) => { 663 | return { type: "index", field }; 664 | } 665 | ), 666 | functionPostfix: (q) => 667 | P.seqMap( 668 | P.string("("), 669 | P.optWhitespace, 670 | q.field.sepBy(P.string(",").trim(P.optWhitespace)), 671 | P.optWhitespace, 672 | P.string(")"), 673 | (_, _1, fields, _2, _3) => { 674 | return { type: "function", fields }; 675 | } 676 | ), 677 | 678 | // The precedence hierarchy of operators - multiply/divide, add/subtract, compare, and then boolean operations. 679 | binaryMulDivField: (q) => 680 | createBinaryParser(q.indexField, q.binaryMulDiv, Fields.binaryOp), 681 | binaryPlusMinusField: (q) => 682 | createBinaryParser(q.binaryMulDivField, q.binaryPlusMinus, Fields.binaryOp), 683 | binaryCompareField: (q) => 684 | createBinaryParser( 685 | q.binaryPlusMinusField, 686 | q.binaryCompareOp, 687 | Fields.binaryOp 688 | ), 689 | binaryBooleanField: (q) => 690 | createBinaryParser( 691 | q.binaryCompareField, 692 | q.binaryBooleanOp, 693 | Fields.binaryOp 694 | ), 695 | binaryOpField: (q) => q.binaryBooleanField, 696 | 697 | field: (q) => q.binaryOpField, 698 | }); 699 | 700 | /** 701 | * Attempt to parse a field from the given text, returning a string error if the 702 | * parse failed. 703 | */ 704 | export function parseField(text: string): Result { 705 | try { 706 | return Result.success(EXPRESSION.field.tryParse(text)); 707 | } catch (error) { 708 | return Result.failure("" + error); 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /src/lib/DataviewConverter.svelte: -------------------------------------------------------------------------------- 1 | 190 | 191 |
192 |
193 |

Dataview to Bases Converter

194 |

Convert Dataview TABLE queries to Obsidian Bases format

195 |
196 | 197 |
198 |
199 |
200 | 201 |
202 | 203 | {#if showExamples} 204 |
205 | {#each examples as example} 206 | 209 | {/each} 210 |
211 | {/if} 212 |
213 |
214 |
215 | 222 |
223 | 224 |
225 |
226 | 230 |
231 | 234 |
235 |
236 | 237 |
238 |
239 | 240 |
241 | {#if basesYaml} 242 | 245 | 246 | 247 | {/if} 248 |
249 |
250 |
251 | 257 |
258 |
259 |
260 | 261 | {#if error} 262 |
263 |

Error:

264 |
{error}
265 |
266 | {/if} 267 | 268 |
269 |
270 |
271 |

How to use

272 |
273 |
274 |
    275 |
  1. Enter a Dataview TABLE query in the left box
  2. 276 |
  3. Choose whether to place filters in views or globally
  4. 277 |
  5. Click "Convert to Bases" to convert it to Bases YAML
  6. 278 |
  7. The converted YAML will appear in the right box
  8. 279 |
  9. You can also try the Bases YAML examples directly
  10. 280 |
  11. Copy the YAML to create a .base file in Obsidian
  12. 281 |
282 |
283 |
284 | 285 |
286 |
287 |

Reference

288 |
289 |
290 |
291 |

Supported Features

292 |
    293 |
  • TABLE fields with aliases
  • 294 |
  • FROM source selection (folders, tags)
  • 295 |
  • WHERE conditions (simple AND, OR)
  • 296 |
  • SORT clause with ASC/DESC
  • 297 |
  • LIMIT clause
  • 298 |
  • GROUP BY clause
  • 299 |
  • Date arithmetic (date + duration, date - duration)
  • 300 |
  • Formulas and calculations in fields
  • 301 |
302 | 303 |

Filter Groups

304 |

Complex filters can be grouped with nested AND/OR logic:

305 |
filters:
306 |   and:
307 |     - condition1
308 |     - or:
309 |         - condition2
310 |         - condition3
311 |     - and:
312 |         - condition4
313 |         - condition5
314 |
315 | 316 |
317 |

Column Order & Sorting

318 |

Bases supports two separate properties for arrangement:

319 |
# Column order (display order in table)
320 | order:
321 |   - column1
322 |   - column2
323 |   - column3
324 | 
325 | # Data sorting (how data is ordered)
326 | sort:
327 |   - column: priority
328 |     direction: DESC
329 |   - column: date
330 |     direction: ASC
331 | 332 |

Date Functions & Arithmetic

333 |

Date expressions and calculations:

334 |
    335 |
  • date(today) - current date
  • 336 |
  • date(tomorrow) - tomorrow's date
  • 337 |
  • date(yesterday) - yesterday's date
  • 338 |
  • date("2024-01-01") - specific date
  • 339 |
  • dur(7 days) - duration literal
  • 340 |
  • date(today) + dur(7 days) - date arithmetic
  • 341 |
  • date(today) - dur(1 week) - subtract duration
  • 342 |
  • due - date(today) - date difference
  • 343 |
  • date.year, date.month, date.day - date accessors
  • 344 |
345 | 346 |

Filter Functions

347 |
    348 |
  • contains()
  • 349 |
  • not_contains()
  • 350 |
  • containsAny()
  • 351 |
  • containsAll()
  • 352 |
  • startswith()
  • 353 |
  • endswith()
  • 354 |
  • empty()
  • 355 |
  • notEmpty()
  • 356 |
  • if()
  • 357 |
  • inFolder()
  • 358 |
  • linksTo()
  • 359 |
  • not()
  • 360 |
  • tag()
  • 361 |
  • dateBefore()
  • 362 |
  • dateAfter()
  • 363 |
  • dateEquals()
  • 364 |
  • dateNotEquals()
  • 365 |
  • dateOnOrBefore()
  • 366 |
  • dateOnOrAfter()
  • 367 |
  • taggedWith()
  • 368 |
369 | 370 |

File Properties

371 |
    372 |
  • file.name - file name
  • 373 |
  • file.path - full file path
  • 374 |
  • file.folder - containing folder
  • 375 |
  • file.extension - file extension
  • 376 |
  • file.size - file size
  • 377 |
  • file.ctime - created time
  • 378 |
  • file.mtime - modified time
  • 379 |
380 |
381 |
382 |
383 |
384 |
385 | 386 | --------------------------------------------------------------------------------