├── src ├── index.ts ├── utils │ └── core.ts ├── data │ ├── types.ts │ ├── common │ │ └── navigation.ts │ ├── watch.ts │ ├── raw-types.ts │ ├── collection.ts │ └── index.ts ├── dom.ts ├── ui │ └── index.ts ├── storage.ts └── date │ └── index.ts ├── package.json ├── .gitignore └── tsconfig.json /src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/core.ts: -------------------------------------------------------------------------------- 1 | export const nonNull = (value: T | null | undefined): value is T => value !== null && value !== undefined; 2 | -------------------------------------------------------------------------------- /src/data/types.ts: -------------------------------------------------------------------------------- 1 | export type ReferenceFilter = { includes: string[]; removes: string[] } 2 | 3 | /** 4 | * todo: this should probably mimic 5 | * block?: { 6 | * string?: string; 7 | * uid?: string; 8 | * open?: boolean; 9 | * heading?: number; 10 | * "text-align"?: TextAlignment; 11 | * "children-view-type"?: ViewType; 12 | * } 13 | */ 14 | export interface BlockData { 15 | text: string 16 | uid?: string 17 | order?: number | 'first' | 'last' // Defaults to 'last' 18 | children?: BlockData[] 19 | open?: boolean 20 | } 21 | -------------------------------------------------------------------------------- /src/dom.ts: -------------------------------------------------------------------------------- 1 | export type TextElement = HTMLTextAreaElement | HTMLInputElement 2 | 3 | export function getActiveEditElement(): TextElement | null { 4 | const element = document.activeElement 5 | 6 | // document.activeElement can be either `document.body` or `null` 7 | // https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/activeElement 8 | if (!element || !isEditElement(element)) return null 9 | 10 | return element 11 | } 12 | 13 | const isEditElement = (element: Element): element is TextElement => 14 | element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' 15 | -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | import getPageUidByPageTitle from 'roamjs-components/queries/getPageUidByPageTitle' 2 | import 'roamjs-components/types' 3 | import {getActiveEditElement} from '../dom' 4 | 5 | export const openPageInSidebar = (name: string) => 6 | window.roamAlphaAPI.ui.rightSidebar.addWindow({ 7 | window: { 8 | 'block-uid': getPageUidByPageTitle(name), 9 | type: 'block', 10 | }, 11 | }) 12 | 13 | export const getSelectionInFocusedBlock = () => ({ 14 | start: getActiveEditElement()?.selectionStart, 15 | end: getActiveEditElement()?.selectionEnd, 16 | }) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roam-api-wrappers", 3 | "version": "0.2.1", 4 | "description": "High level API wrappers for RoamResearch API", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "git@github.com:Stvad/roam-api-wrappers.git", 8 | "author": "Vlad Sitalo", 9 | "license": "Apache-2.0", 10 | "private": false, 11 | "scripts": { 12 | "build": "npm run clean && npm run compile", 13 | "clean": "rm -rf dist", 14 | "compile": "tsc", 15 | "prepublishOnly": "npm run build", 16 | "watch": "npm run compile -- --watch" 17 | }, 18 | "dependencies": { 19 | "lodash-es": "^4.17.21", 20 | "roamjs-components": "^0.75.1" 21 | }, 22 | "devDependencies": { 23 | "@types/lodash-es": "^4.14.184", 24 | "typescript": "^4.8.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/data/common/navigation.ts: -------------------------------------------------------------------------------- 1 | import getPageTitleByPageUid from 'roamjs-components/queries/getPageTitleByPageUid' 2 | 3 | export const Navigation = { 4 | get currentPageUid() { 5 | const parts = new URL(window.location.href).hash.split("/") 6 | return parts[parts.length - 1] 7 | }, 8 | 9 | get currentPageName() { 10 | return getPageTitleByPageUid(this.currentPageUid) 11 | }, 12 | 13 | get currentUrl() { 14 | return new URL(window.location.href) 15 | }, 16 | 17 | get baseUrl() { 18 | // https://roamresearch.com/#/app/roam-toolkit/page/03-24-2020 19 | const url = this.currentUrl 20 | const parts = url.hash.split("/") 21 | 22 | url.hash = parts.slice(0, 3).join("/") 23 | return url 24 | }, 25 | 26 | get basePageUrl() { 27 | const url = this.baseUrl 28 | url.hash = url.hash.concat("/page") 29 | return url 30 | }, 31 | 32 | urlForUid(uid: string) { 33 | return this.basePageUrl.toString() + "/" + uid 34 | }, 35 | 36 | goToUid(uid: string) { 37 | window.location.href = this.urlForUid(uid) 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import {Page} from './data' 2 | 3 | interface AsyncStorage { 4 | get(key: string): Promise 5 | set(key: string, value: string): Promise 6 | } 7 | 8 | export class ObjectStorage { 9 | constructor(private storage: AsyncStorage) { 10 | } 11 | 12 | async get(key: string, defaultValue?: T): Promise { 13 | const str = await this.storage.get(key) 14 | return str ? JSON.parse(str) as T : defaultValue 15 | } 16 | 17 | set = (key: string, value: T) => 18 | this.storage.set(key, JSON.stringify(value)) 19 | } 20 | 21 | export class RoamStorage implements AsyncStorage { 22 | constructor(private pageName: string) { 23 | } 24 | 25 | async get(key: string): Promise { 26 | return Page.fromName(this.pageName)?.childWithValue(key)?.children[0]?.text ?? null 27 | } 28 | 29 | async set(key: string, value: string) { 30 | const page = Page.fromName(this.pageName)! 31 | const block = await page.childAtPath([key, '0'], true) 32 | block!.text = value 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/date/index.ts: -------------------------------------------------------------------------------- 1 | import format from "date-fns/format"; 2 | import parse from "date-fns/parse"; 3 | 4 | export const parseRoamDate = (s: string) => 5 | parse(s, "MMMM do, yyyy", new Date()); 6 | 7 | export const parseRoamDateUid = (s: string) => 8 | parse(s, "MM-dd-yyyy", new Date()); 9 | 10 | export const toRoamDate = (d: Date) => 11 | isNaN(d.valueOf()) ? "" : format(d, "MMMM do, yyyy"); 12 | 13 | export const toRoamDateUid = (d: Date) => 14 | isNaN(d.valueOf()) ? "" : format(d, "MM-dd-yyyy"); 15 | 16 | export const DAILY_NOTE_PAGE_REGEX = /(January|February|March|April|May|June|July|August|September|October|November|December) [0-3]?[0-9](st|nd|rd|th), [0-9][0-9][0-9][0-9]/; 17 | export const DAILY_NOTE_PAGE_TITLE_REGEX = new RegExp( 18 | `^${DAILY_NOTE_PAGE_REGEX.source}$` 19 | ); 20 | 21 | export const RoamDate = { 22 | regex: DAILY_NOTE_PAGE_REGEX, 23 | onlyPageTitleRegex: DAILY_NOTE_PAGE_TITLE_REGEX, 24 | referenceRegex: new RegExp(`\\[\\[${DAILY_NOTE_PAGE_REGEX.source}]]`, 'gm'), 25 | parse: parseRoamDate, 26 | toRoam: toRoamDate, 27 | toDatePage(d: Date) { 28 | return "[[" + this.toRoam(d) + "]]" 29 | }, 30 | parseFromReference(name: string): Date { 31 | return this.parse(name.slice(2).slice(0, -2)) 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /src/data/watch.ts: -------------------------------------------------------------------------------- 1 | // import {PullBlock} from "../types" 2 | import {Block} from './index' 3 | import {PullBlock} from 'roamjs-components/types' 4 | 5 | export interface WatchBlock { 6 | ':block/string': string 7 | ':block/uid': string 8 | ':block/children'?: WatchBlock[] 9 | } 10 | 11 | /** 12 | * Returns disconnect/stop watching function 13 | * 14 | * Todo in order to get reactivity (including watching children getting some inefficiency now 15 | * 1. watching every value downstream 16 | * 2. re-fetching the blocks on each change vs using supplied data 17 | */ 18 | export const watchTree = (blockId: string, callback: (before: WatchBlock, after: Block) => void) => { 19 | // Retrieving after in custom fashion to comply with expected shape of data for Block class 20 | const wrappedCallback = (before: PullBlock | null, _: PullBlock | null) => 21 | callback(before as WatchBlock, Block.fromUid(blockId)!) 22 | 23 | /** 24 | * Todo 25 | * Right no changes to order/open state are ignored, should I watch for them too? 26 | */ 27 | 28 | const pullPattern = '[:block/children :block/string :block/uid {:block/children ...}]' 29 | const entityId = `[:block/uid "${blockId}"]` 30 | 31 | window.roamAlphaAPI 32 | .data 33 | .addPullWatch(pullPattern, entityId, wrappedCallback) 34 | 35 | return () => 36 | window.roamAlphaAPI 37 | .data 38 | .removePullWatch(pullPattern, entityId, wrappedCallback) 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | -------------------------------------------------------------------------------- /src/data/raw-types.ts: -------------------------------------------------------------------------------- 1 | export type RoamNode = { ":db/id": number } 2 | export type RawViewType = ":bullet" | ":document" | ":numbered" 3 | /** 4 | * looks like [ ":block/uid", "2PQN2a7g8" ] irl 5 | */ 6 | export type RawAttrKeyValue = string[] 7 | 8 | export interface RawAttrReferenceKeyValue { 9 | ":source": RawAttrKeyValue 10 | ":value": RawAttrKeyValue 11 | } 12 | 13 | export interface RawBlockProps { 14 | ":slider": any 15 | ":image-size": any 16 | ":encryption": any 17 | ":POMO": any 18 | ":diagram": any 19 | } 20 | 21 | export interface RawRoamPage extends RoamNode { 22 | ":attrs/lookup"?: RoamNode[] 23 | ":block/children": RoamNode[] 24 | ":block/open"?: boolean 25 | ":block/refs"?: RoamNode[] 26 | ":block/uid": string 27 | ":children/view-type"?: RawViewType 28 | ":create/time": number 29 | ":create/user": RoamNode 30 | ":edit/seen-by": RoamNode[] 31 | ":edit/time": number 32 | ":edit/user": RoamNode 33 | ":entity/attrs"?: RawAttrReferenceKeyValue 34 | ":log/id"?: number 35 | ":node/title": string 36 | /** 37 | * TODO: need more examples. one I have is { ":public": "readonly" } 38 | */ 39 | ":page/permissions": any 40 | ":page/sidebar"?: number 41 | } 42 | 43 | export interface RawRoamBlock extends RoamNode { 44 | ":attrs/lookup"?: RoamNode[] 45 | ":block/children": RoamNode[] 46 | ":block/open"?: boolean 47 | ":block/refs"?: RoamNode[] 48 | ":block/uid": string 49 | ":children/view-type"?: RawViewType 50 | ":create/time": number 51 | ":create/user": RoamNode 52 | ":edit/seen-by": RoamNode[] 53 | ":edit/time": number 54 | ":edit/user": RoamNode 55 | ":entity/attrs"?: RawAttrReferenceKeyValue 56 | ":log/id"?: number 57 | ":node/title": string 58 | /** 59 | * TODO: need more examples. one I have is { ":public": "readonly" } 60 | */ 61 | ":page/permissions": any 62 | ":page/sidebar"?: number 63 | 64 | ":block/heading"?: 0 | 1 | 2 | 3 65 | ":block/order"?: number 66 | ":block/page": RoamNode 67 | ":block/parents"?: RoamNode[] 68 | ":block/props"?: RawBlockProps 69 | ":block/string": string 70 | ":block/text-align"?: "left" | "center" | "right" | "justify" 71 | ":create/email"?: string 72 | ":ent/emojis": any[] 73 | ":vc/blocks"?: RoamNode[] 74 | 75 | } 76 | 77 | export interface RawUser extends RoamNode { 78 | ":user/color": string 79 | ":user/display-name": string 80 | ":user/email": string 81 | ":user/photo-url": string 82 | ":user/settings": any 83 | ":user/uid": string 84 | ":block/uid": string 85 | } 86 | 87 | export interface RawPageFilter extends RoamNode { 88 | ":window/filters": string 89 | ":window/id": string 90 | ":block/uid": string 91 | } 92 | -------------------------------------------------------------------------------- /src/data/collection.ts: -------------------------------------------------------------------------------- 1 | import {Page, RoamEntity} from './index' 2 | import {ReferenceFilter} from './types' 3 | import {RoamDate} from '../date' 4 | import {groupBy, partition} from 'lodash-es' 5 | 6 | export const defaultExclusions = [ 7 | /^ptr$/, 8 | /^otter\.ai\/transcript$/, 9 | /^otter\.ai$/, 10 | /^TODO$/, 11 | /^DONE$/, 12 | /^factor$/, 13 | /^interval$/, 14 | /^\[\[factor]]:.+/, 15 | /^\[\[interval]]:.+/, 16 | /^isa$/, 17 | /^repeat interval$/, 18 | /^make-public$/, 19 | /^matrix-messages$/, 20 | RoamDate.onlyPageTitleRegex, 21 | ] 22 | 23 | export const defaultLowPriority = [ 24 | /^reflection$/, 25 | /^task$/, 26 | /^weekly review$/, 27 | /^person$/, 28 | ] 29 | 30 | type Priority = 'high' | 'default' | 'low' 31 | 32 | const isPartOfHierarchy = (ref: RoamEntity) => ref instanceof Page && ref.text?.includes('/') 33 | 34 | export const getGroupsForEntity = ( 35 | entity: RoamEntity, 36 | { 37 | addReferencesBasedOnAttributes = ['isa', 'group with'], 38 | dontGroupReferencesTo = defaultExclusions, 39 | }: { 40 | addReferencesBasedOnAttributes?: string[], 41 | dontGroupReferencesTo?: RegExp[], 42 | } = {} 43 | ): RoamEntity[] => { 44 | const notExcluded = (entity: RoamEntity) => !dontGroupReferencesTo.some(it => it.test(entity.text)) 45 | 46 | const getReferencesFromHierarchy = (ref: RoamEntity) => { 47 | if (!isPartOfHierarchy(ref)) return [] 48 | 49 | return ref.text.split('/') 50 | .slice(0, -1) 51 | .map((_, i, arr) => Page.fromName(arr.slice(0, i + 1).join('/'))) 52 | .filter(levelReference => levelReference && notExcluded(levelReference)) as RoamEntity[] 53 | } 54 | 55 | const getReferencesFromAttribute = (baseReference: RoamEntity, attribute: string) => { 56 | const isNotAttributeReference = (it: RoamEntity) => it.text !== attribute 57 | 58 | return baseReference.firstAttributeBlock(attribute) 59 | ?.linkedEntities.filter(isNotAttributeReference).filter(notExcluded) ?? [] 60 | } 61 | 62 | const getEntityGroupsFromReference = (reference: RoamEntity, entity: RoamEntity): RoamEntity[] => [ 63 | reference, 64 | ...getReferencesFromHierarchy(reference), 65 | ...addReferencesBasedOnAttributes.flatMap(attribute => getReferencesFromAttribute(reference, attribute)), 66 | ] 67 | 68 | const linkedEntities = [...entity.getLinkedEntities(true), entity.page] 69 | const filteredReferences = linkedEntities.filter(notExcluded) 70 | 71 | return filteredReferences.flatMap(reference => getEntityGroupsFromReference(reference, entity)) 72 | } 73 | 74 | /** 75 | * what I want is something like: 76 | * - create groups based on the most common page reference among all entities, excluding the things like factor, interval, TODO, DONE, etc 77 | * if one group is wholly contained within another - preserve the smaller group inside the larger group 78 | * 79 | * not sure about an efficient way to do this, but starting with creating all groups may be ok, given that we only run over like ~100 elements 80 | * 81 | * exclude the current page 82 | * ptr, otter transcript 83 | */ 84 | export class CommonReferencesGrouper { 85 | constructor( 86 | /** 87 | * If it doesn't fit into any other group - put it here 88 | */ 89 | private fallbackGroup: string, 90 | // plausibly this should be "really low priority" as in - after all possible other groups have been created 91 | // don't leave things ungrouped if they can go into one of excluded categories 92 | private dontGroupReferencesTo: RegExp[] = defaultExclusions, 93 | private groupPriorities: Record, RegExp[]> = { 94 | low: [...defaultExclusions, ...defaultLowPriority], 95 | high: [], 96 | }, 97 | private addReferencesBasedOnAttributes: string[] = ['isa', 'group with'], 98 | ) { 99 | } 100 | 101 | public group(entities: RoamEntity[]): Map { 102 | // todo when we exclude all the things - just return one group 103 | 104 | // todo how important is dedup? (would it actually be better to show a few larger groups that have overlap?) 105 | // todo merge groups that overlap exactly 106 | const referenceGroups = this.buildReferenceGroups(entities) 107 | 108 | return this.deduplicateAndSortGroups(referenceGroups) 109 | } 110 | 111 | private buildReferenceGroups(entities: RoamEntity[]) { 112 | const referenceGroups = new Map>() 113 | 114 | function addReferenceToGroup(referenceUid: string, entity: RoamEntity) { 115 | const group = referenceGroups.get(referenceUid) 116 | if (group) { 117 | group.set(entity.uid, entity) 118 | } else { 119 | referenceGroups.set(referenceUid, new Map([[entity.uid, entity]])) 120 | } 121 | } 122 | 123 | for (const entity of entities) { 124 | const groupsForEntity = getGroupsForEntity(entity, 125 | { 126 | addReferencesBasedOnAttributes: this.addReferencesBasedOnAttributes, 127 | dontGroupReferencesTo: this.dontGroupReferencesTo, 128 | }) 129 | groupsForEntity.forEach(ref => addReferenceToGroup(ref.uid, entity)) 130 | 131 | if (!groupsForEntity.length) addReferenceToGroup(this.fallbackGroup, entity) 132 | } 133 | return referenceGroups 134 | } 135 | 136 | /** 137 | * take the largest group out, 138 | * and remove its members from all other groups, which would re-balance the groups 139 | * also a good place to find the wholly subsumed groups (they'd end up empty) 140 | * 141 | * given how this goes, probably doesn't really make sense to sort the sets or something, plausibly heap would help but also as likely to require too much updating 142 | */ 143 | deduplicateAndSortGroups( 144 | referenceGroups: Map>, 145 | ) { 146 | const groupsByPriorities = groupGroupsByPriorities(referenceGroups, this.groupPriorities) as Record][]> 147 | console.log({groupsByPriorities}) 148 | 149 | const result: Array = [] 150 | 151 | function consumeFrom(priorityGroup: Map>, minGroupSize: number = 1) { 152 | while (referenceGroups.size && priorityGroup.size) { 153 | const [referenceUid, largestGroup] = pickLargest(priorityGroup) 154 | if (largestGroup.size < minGroupSize) break 155 | 156 | result.push([referenceUid, getValues(largestGroup)] as const) 157 | 158 | priorityGroup.delete(referenceUid) 159 | referenceGroups.delete(referenceUid) 160 | 161 | removeGroupEntriesFromOtherGroups(referenceGroups, largestGroup) 162 | } 163 | } 164 | 165 | consumeFrom(new Map(groupsByPriorities.high)) 166 | consumeFrom(new Map(groupsByPriorities.default), 2) 167 | consumeFrom(new Map(groupsByPriorities.low)) 168 | 169 | // consume the rest 170 | consumeFrom(referenceGroups) 171 | 172 | return new Map([...result]) 173 | } 174 | } 175 | 176 | export const mergeGroupsSmallerThan = ( 177 | referenceGroups: Map, 178 | intoKey: string, 179 | minGroupSize: number, 180 | dontMerge: (uid: string) => boolean, 181 | ) => { 182 | const [small, large] = 183 | partition([...referenceGroups.entries()], 184 | ([key, group]) => !dontMerge(key) && (group.length < minGroupSize || key === intoKey)) 185 | 186 | const mergedItems = small.map(([_, group]) => group).flat() 187 | return new Map([...large, [intoKey, mergedItems]]) 188 | } 189 | 190 | const getValues = (largestGroup: Map) => Array.from(largestGroup.values()) 191 | 192 | function removeGroupEntriesFromOtherGroups( 193 | referenceGroups: Map>, 194 | largestGroup: Map, 195 | ) { 196 | for (const [_, group] of referenceGroups) { 197 | // todo remove empty groups 198 | if (group.size === 0) continue 199 | 200 | for (const uid of largestGroup.keys()) { 201 | group.delete(uid) 202 | } 203 | } 204 | } 205 | 206 | const groupGroupsByPriorities = ( 207 | referenceGroups: Map>, 208 | groupPriorities: Record<'high' | 'low', RegExp[]>, 209 | ) => 210 | groupBy([...referenceGroups], 211 | ([refUid]: [string, Map]): Priority => { 212 | const ref = RoamEntity.fromUid(refUid)! 213 | if (groupPriorities.high.some(it => it.test(ref.text))) return 'high' 214 | if (groupPriorities.low.some(it => it.test(ref.text))) return 'low' 215 | return 'default' 216 | }) 217 | 218 | const pickLargest = (referenceGroups: Map>) => 219 | [...referenceGroups.entries()] 220 | .reduce((a, b) => a[1].size > b[1].size ? a : b) 221 | 222 | export function matchesFilter(entity: RoamEntity, filters: ReferenceFilter) { 223 | const refs = entity.getLinkedEntities(true) 224 | 225 | const matchesAllIncludes = filters.includes.every((f) => refs.some((r) => r.text === f)) 226 | const matchesNoRemoves = filters.removes.every((f) => !refs.some((r) => r.text === f)) 227 | 228 | return matchesAllIncludes && matchesNoRemoves 229 | } 230 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2022", 15 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 16 | "lib": [ 17 | "ES2021", 18 | "dom" 19 | ], 20 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 21 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 22 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 23 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 24 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 25 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 26 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 27 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 28 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 29 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 30 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 31 | 32 | /* Modules */ 33 | "module": "ES2022", 34 | /* Specify what module code is generated. */ 35 | // "rootDir": "./", /* Specify the root folder within your source files. */ 36 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 37 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 38 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 39 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 40 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 41 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 42 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 43 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 46 | 47 | /* JavaScript Support */ 48 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 49 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 51 | 52 | /* Emit */ 53 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 54 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, 81 | /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 83 | "forceConsistentCasingInFileNames": true, 84 | /* Ensure that casing is correct in imports. */ 85 | 86 | /* Type Checking */ 87 | "strict": true, 88 | /* Enable all strict type-checking options. */ 89 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 90 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 91 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 92 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 93 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 94 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 95 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 96 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 97 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 98 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 99 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 100 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 101 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 102 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 103 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 104 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 105 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 106 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 107 | 108 | /* Completeness */ 109 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 110 | "skipLibCheck": true, 111 | /* Skip type checking all .d.ts files. */ 112 | "outDir": "dist" 113 | }, 114 | "include": [ 115 | "src/**/*.ts" 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | import {RawRoamBlock, RawRoamPage, RoamNode} from './raw-types' 2 | import {Navigation} from './common/navigation' 3 | 4 | import {countBy} from 'lodash-es' 5 | 6 | import getBlockUidsReferencingPage from 'roamjs-components/queries/getBlockUidsReferencingPage' 7 | import getBlockUidsReferencingBlock from 'roamjs-components/queries/getBlockUidsReferencingBlock' 8 | import {nonNull} from '../utils/core' 9 | import {BlockData, ReferenceFilter} from './types' 10 | 11 | const DEFAULT_INSERT_ORDER = 'last' 12 | 13 | export const Roam = { 14 | query(query: string, ...params: any[]): any[] { 15 | return window.roamAlphaAPI.q(query, ...params) 16 | }, 17 | pull(dbid: number | string, selector = '[*]'): RawRoamPage | RawRoamBlock | null { 18 | if (!dbid) { 19 | console.log('bad id') 20 | return null 21 | } 22 | //@ts-ignore TODO reconcile types 23 | return window.roamAlphaAPI.pull(selector, dbid) 24 | }, 25 | 26 | pullByUid(uid: string, selector = '[*]'): RawRoamPage | RawRoamBlock | null { 27 | return this.pull(`[:block/uid "${uid}"]`, selector) 28 | }, 29 | 30 | queryFirst(query: string, ...params: any[]) { 31 | const results = this.query(query, ...params) 32 | if (!results?.[0] || results?.[0].length < 1) return null 33 | 34 | return this.pull(results[0][0]) 35 | }, 36 | 37 | listPageIds() { 38 | return this.query('[:find ?page :where [?page :node/title ?title] [?page :block/uid ?uid]]').flat() 39 | }, 40 | 41 | listPages(): RawRoamPage[] { 42 | return this.listPageIds().map((dbId: number) => this.pull(dbId)!) 43 | }, 44 | 45 | getUid(node: RoamNode) { 46 | return this.pull(node[':db/id'])?.[':block/uid'] 47 | }, 48 | 49 | focusedBlockInfo() { 50 | return window.roamAlphaAPI.ui.getFocusedBlock() 51 | }, 52 | } 53 | 54 | function createAttributeString(name: string, value: string) { 55 | return `${name}::${value}` 56 | } 57 | 58 | export abstract class RoamEntity { 59 | 60 | static fromUid(uid: string): Page | Block | null { 61 | const rawEntity = Roam.pullByUid(uid) 62 | if (!rawEntity) return null 63 | 64 | return RoamEntity.fromRaw(rawEntity) 65 | } 66 | 67 | static fromRaw(rawEntity: RawRoamBlock | RawRoamPage) { 68 | if (rawEntity[':node/title']) return new Page(rawEntity) 69 | return new Block(rawEntity as RawRoamBlock) 70 | } 71 | 72 | constructor(readonly rawEntity: RawRoamBlock | RawRoamPage) { 73 | return new Proxy(this, { 74 | get(origin, property: keyof RoamEntity | string) { 75 | if (property in origin) return origin[property as keyof RoamEntity] 76 | 77 | return origin.child(property) 78 | }, 79 | }) 80 | } 81 | 82 | abstract get text(): string 83 | abstract set text(value: string) 84 | 85 | get rawChildren(): RawRoamBlock[] { 86 | const children = this.rawEntity[':block/children']?.map(it => Roam.pull(it[':db/id'])) as RawRoamBlock[] 87 | /** 88 | * Sorted because the order of the children returned is ~arbitrary 89 | */ 90 | return children?.sort((a, b) => a[':block/order']! - b[':block/order']!) || [] 91 | } 92 | 93 | get children(): Block[] { 94 | return this.rawChildren.map(it => new Block(it)) 95 | } 96 | 97 | abstract get parent(): RoamEntity | null 98 | 99 | abstract get parents(): RoamEntity[] 100 | 101 | get uid(): string { 102 | return this.rawEntity[':block/uid'] 103 | } 104 | 105 | get url(): string { 106 | return Navigation.urlForUid(this.uid) 107 | } 108 | 109 | get createdTime(): number { 110 | return this.rawEntity[':create/time'] 111 | } 112 | 113 | /** 114 | * The desired effect is to be able to get child blocks either by content or by order 115 | * block[number] would give you children by order (block[0] is a first child) 116 | * block.content or block["content"] would give you a child by content 117 | * 118 | * Todo potentially allow accessing the roam attributes without having to specify `::` at the end 119 | * Todo can i support regex selectors? - maybe. would require custom parsing though, everythign I get is a string =\ 120 | */ 121 | child(property: string): Block | Block[] | null { 122 | const idx = parseInt(property) 123 | if (Number.isInteger(idx)) return this.children?.[idx] 124 | 125 | //todo check for regex stuff explicitly 126 | return this.childWithValue(property) || 127 | this.childrenMatching(new RegExp(`^${property}::`))?.[0] || 128 | this.childrenMatching(new RegExp(property)) 129 | } 130 | 131 | childWithIndexOrValue(indexOrValue: string): Block | null { 132 | const idx = parseInt(indexOrValue) 133 | if (Number.isInteger(idx)) return this.children?.[idx] 134 | 135 | return this.childWithValue(indexOrValue) 136 | } 137 | 138 | childWithValue(content: string): Block | null { 139 | return this.children?.find(it => it.text === content) ?? null 140 | } 141 | 142 | async childAtPath(path: string[], createIfMissing = false): Promise { 143 | let block: Block | RoamEntity = this 144 | for (const part of path) { 145 | const existing: Block | null = block.childWithIndexOrValue(part) 146 | if (existing) { 147 | block = existing 148 | continue 149 | } 150 | 151 | if (!createIfMissing) return null 152 | 153 | block = await block.appendChild(part) 154 | } 155 | 156 | return block as Block 157 | } 158 | 159 | childrenMatching(regex: RegExp) { 160 | const result = this.children?.filter(it => regex.test(it.text)) 161 | return result?.length ? result : null 162 | } 163 | 164 | get rawLinkedEntities(): (RawRoamPage | RawRoamBlock)[] { 165 | return this.rawEntity[':block/refs']?.map(it => Roam.pull(it[':db/id'])).filter(nonNull) ?? [] 166 | } 167 | 168 | get linkedEntities(): RoamEntity[] { 169 | return this.getLinkedEntities() 170 | } 171 | 172 | getLinkedEntities(includeRefsFromParents: boolean = false): RoamEntity[] { 173 | const local = this.rawLinkedEntities.map(it => RoamEntity.fromRaw(it)) 174 | const fromParents = includeRefsFromParents ? 175 | (this.parents.flatMap(it => it?.getLinkedEntities() ?? [])) : 176 | [] 177 | return [...local, ...fromParents] 178 | } 179 | 180 | setAttribute(name: string, value: string) { 181 | const existing = this.child(name) as Block 182 | if (existing) { 183 | existing.setAsAttribute(name, value) 184 | return 185 | } 186 | 187 | return this.appendChild(createAttributeString(name, value)) 188 | } 189 | 190 | setAsAttribute(name: string, value: string) { 191 | this.text = createAttributeString(name, value) 192 | } 193 | 194 | firstAttributeBlock(name: string): Block | undefined { 195 | return this.getAttributeBlocks(name)[0] 196 | } 197 | 198 | getAttributeBlocks(name: string): Block[] { 199 | // todo roam actually parses the attributes separately, so I probably should use that 200 | return this.childrenMatching(new RegExp(`^${name}::`)) ?? [] 201 | } 202 | 203 | 204 | /** 205 | * Preferred version to use 206 | */ 207 | async appendChild(childData: BlockData): Promise; 208 | /** 209 | * @deprecated use appendTextChild instead 210 | */ 211 | async appendChild(childData: string): Promise; 212 | async appendChild(childData: string | BlockData): Promise { 213 | if (typeof childData === 'string') return this.appendTextChild(childData) 214 | 215 | return this.insertChild(childData) 216 | } 217 | 218 | async insertChild(childData: BlockData): Promise { 219 | const newUid = childData.uid || window.roamAlphaAPI.util.generateUID() 220 | await window.roamAlphaAPI.createBlock({ 221 | location: { 222 | 'parent-uid': this.uid, 223 | // @ts-ignore new thing 224 | order: childData.order || DEFAULT_INSERT_ORDER, 225 | }, 226 | block: { 227 | string: childData.text, 228 | uid: newUid, 229 | open: childData.open, 230 | }, 231 | }) 232 | 233 | const childBlock = Block.fromUid(newUid) 234 | childData.children?.forEach(it => childBlock.insertChild(it)) 235 | 236 | return childBlock 237 | } 238 | 239 | async appendTextChild(text: string, uid?: string): Promise { 240 | return this.insertChild({text, uid}) 241 | } 242 | 243 | get backlinks(): RoamEntity[] { 244 | const backlinks = getBlockUidsReferencingBlock(this.uid) 245 | return backlinks.map(it => RoamEntity.fromUid(it)).filter(nonNull) 246 | } 247 | 248 | abstract get referenceFilter(): ReferenceFilter 249 | 250 | abstract get page(): Page 251 | } 252 | 253 | export class Page extends RoamEntity { 254 | constructor(rawPage: RawRoamPage) { 255 | super(rawPage) 256 | } 257 | 258 | get rawPage(): RawRoamPage { 259 | return this.rawEntity as RawRoamPage 260 | } 261 | 262 | static fromName(name: string) { 263 | const rawPage = Roam.queryFirst('[:find ?e :in $ ?a :where [?e :node/title ?a]]', name) 264 | return rawPage ? new this(rawPage) : null 265 | } 266 | 267 | static getOrCreate(name: string) { 268 | return this.fromName(name) || this.new(name) 269 | } 270 | 271 | static async new(name: string) { 272 | await window.roamAlphaAPI.createPage({ 273 | page: { 274 | title: name, 275 | }, 276 | }) 277 | return Page.fromName(name)! 278 | } 279 | 280 | get text(): string { 281 | return this.rawPage[':node/title'] 282 | } 283 | 284 | set text(value: string) { 285 | window.roamAlphaAPI.updatePage({ 286 | page: { 287 | uid: this.uid, 288 | title: value, 289 | }, 290 | }) 291 | } 292 | 293 | get parent(): RoamEntity | null { 294 | return null 295 | } 296 | 297 | get parents(): RoamEntity[] { 298 | return [] 299 | } 300 | 301 | get referenceFilter(): ReferenceFilter { 302 | //@ts-ignore todo types 303 | return window.roamAlphaAPI.ui.filters.getPageLinkedRefsFilters({page: {title: this.text}}) 304 | } 305 | 306 | get page(): Page { 307 | return this 308 | } 309 | } 310 | 311 | export class Attribute extends Page { 312 | getUniqueValues(): Set { 313 | return new Set(this.getAllValues()) 314 | } 315 | 316 | getAllValues(): string[] { 317 | return getBlockUidsReferencingPage(this.text) 318 | .map(Block.fromUid) 319 | .flatMap(it => it?.listAttributeValues() || []) 320 | } 321 | 322 | getValuesByCount() { 323 | const allValues = this.getAllValues() 324 | return Object.entries(countBy(allValues)) 325 | .sort(([, a], [, b]) => (a as number) - (b as number)).reverse() 326 | } 327 | 328 | findBlocksWithValue(value: string): Block[] { 329 | //todo compare perf of querying for "contains 2 pages" 330 | const attributeBlocks = getBlockUidsReferencingPage(this.text) 331 | const valuePageBlocks = new Set(getBlockUidsReferencingPage(value)) 332 | let intersect = new Set(attributeBlocks.filter(i => valuePageBlocks.has(i))) 333 | 334 | //todo not exactly correct 335 | return [...intersect].map(Block.fromUid).filter(Boolean) as Block[] 336 | // return getBlockUidsReferencingPage("isa") 337 | // .map(Block.fromUid) 338 | // .filter(it => it?.listAttributeValues().includes(value)) 339 | 340 | } 341 | 342 | 343 | } 344 | 345 | 346 | export class Block extends RoamEntity { 347 | constructor(rawBlock: RawRoamBlock) { 348 | super(rawBlock) 349 | } 350 | 351 | static get current() { 352 | const focusedBlockUid = Roam.focusedBlockInfo()?.['block-uid'] 353 | return focusedBlockUid ? Block.fromUid(focusedBlockUid) : null 354 | } 355 | 356 | get rawBlock(): RawRoamBlock { 357 | return this.rawEntity as RawRoamBlock 358 | } 359 | 360 | static fromUid(uid: string) { 361 | return RoamEntity.fromUid(uid) as Block 362 | } 363 | 364 | get text(): string { 365 | return this.rawBlock[':block/string'] 366 | } 367 | 368 | set text(value: string) { 369 | window.roamAlphaAPI.updateBlock({ 370 | block: { 371 | uid: this.uid, 372 | string: value, 373 | }, 374 | }) 375 | } 376 | 377 | get parent(): RoamEntity | null { 378 | const parentIds = this.rawBlock[':block/parents'] 379 | if (!parentIds) return null 380 | 381 | // TODO: unreliable this ends up not being correct for Readwise created nodes 🤔 382 | const directParentId = parentIds[parentIds.length - 1] 383 | 384 | const rawParent = directParentId && Roam.pull(directParentId[':db/id']) 385 | return rawParent ? RoamEntity.fromRaw(rawParent) : null 386 | } 387 | 388 | get parents(): RoamEntity[] { 389 | const parentIds = this.rawBlock[':block/parents'] 390 | if (!parentIds) return [] 391 | 392 | return parentIds.map(it => RoamEntity.fromRaw(Roam.pull(it[':db/id'])!)) 393 | } 394 | 395 | get containerPage(): Page { 396 | return new Page(Roam.pull(this.rawBlock[':block/page'][':db/id'])!) 397 | } 398 | 399 | /** 400 | * Attribute value is weird - can be any of the children or the same-line value 401 | */ 402 | get attributeValue(): string | undefined { 403 | return this.text.split('::')[1]?.trim() 404 | } 405 | 406 | get definesAttribute(): boolean { 407 | return this.text.includes('::') 408 | } 409 | 410 | listAttributeValues(splitRegex?: RegExp): string[] { 411 | if (!this.definesAttribute) return [] 412 | 413 | // todo do we just want text values? 414 | const childrenValues = this.children.map(it => it.text) 415 | 416 | 417 | // todo doing this vs default value, because this breaks safari 418 | // which does not support lookbehind =\ (roam-date bug) 419 | const defaultRegex = new RegExp('(?<=])\\s?(?=\\[)', 'g') 420 | const inPlaceValues = this.listInPlaceAttributeValues(splitRegex ? splitRegex : defaultRegex) 421 | 422 | return [...inPlaceValues, ...childrenValues] 423 | } 424 | 425 | listInPlaceAttributeValues(splitRegex: RegExp) { 426 | const valueStr = this.text.split('::')[1]?.trim() 427 | return valueStr?.split(splitRegex)?.filter(it => it) || [] 428 | } 429 | 430 | get referenceFilter(): ReferenceFilter { 431 | throw new Error('Not supported by Roam') 432 | } 433 | 434 | get page(): Page { 435 | return new Page(Roam.pull(this.rawBlock[':block/page'][':db/id'])!)! 436 | } 437 | } 438 | 439 | export {matchesFilter} from './collection' 440 | --------------------------------------------------------------------------------