feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collect, hasNode, paragraphHasCheckbox, textIncludes } from './midstate-utils'
|
||||
|
||||
describe('midstate-utils', () => {
|
||||
it('collect finds nested nodes of a given type', () => {
|
||||
const nodes = [
|
||||
{ type: 'paragraph', children: [{ type: 'text', content: 'a' }] },
|
||||
{ type: 'list', items: [{ type: 'list_item', children: [{ type: 'paragraph', children: [{ type: 'text', content: 'b' }] }] }] },
|
||||
{ type: 'custom', children: [{ type: 'emphasis', children: [{ type: 'text', content: 'x' }] }] },
|
||||
]
|
||||
const paras = collect(nodes as any, 'paragraph')
|
||||
expect(paras.length).toBeGreaterThanOrEqual(1)
|
||||
const emph = collect(nodes as any, 'emphasis')
|
||||
expect(emph.length).toBe(1)
|
||||
})
|
||||
|
||||
it('hasNode returns true when node exists', () => {
|
||||
const nodes = [{ type: 'a' }, { type: 'b' }, { type: 'c' }]
|
||||
expect(hasNode(nodes as any, 'b')).toBe(true)
|
||||
expect(hasNode(nodes as any, 'z')).toBe(false)
|
||||
})
|
||||
|
||||
it('textIncludes works on simple structures', () => {
|
||||
const nodes = [{ type: 'paragraph', children: [{ type: 'text', content: 'hello world' }] }]
|
||||
expect(textIncludes(nodes as any, 'hello')).toBe(true)
|
||||
expect(textIncludes(nodes as any, 'bye')).toBe(false)
|
||||
})
|
||||
|
||||
it('textIncludes supports RegExp matching', () => {
|
||||
const nodes = [{ type: 'paragraph', children: [{ type: 'text', content: 'Hello WORLD' }] }]
|
||||
expect(textIncludes(nodes as any, /WORLD$/i)).toBe(true)
|
||||
expect(textIncludes(nodes as any, /nope/)).toBe(false)
|
||||
})
|
||||
|
||||
it('paragraphHasCheckbox detects checkbox children', () => {
|
||||
const p = { type: 'paragraph', children: [{ type: 'checkbox', checked: true }, { type: 'text', content: 'hi' }] }
|
||||
expect(paragraphHasCheckbox([p] as any)).toBe(true)
|
||||
const p2 = { type: 'paragraph', children: [{ type: 'text', content: 'nope' }] }
|
||||
expect(paragraphHasCheckbox([p2] as any)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
export function collect(nodes: any[], type: string) {
|
||||
const out: any[] = []
|
||||
const walk = (n: any) => {
|
||||
if (!n)
|
||||
return
|
||||
if (Array.isArray(n))
|
||||
return n.forEach(walk)
|
||||
if (n.type === type)
|
||||
out.push(n)
|
||||
if (Array.isArray(n.children))
|
||||
n.children.forEach(walk)
|
||||
if (Array.isArray(n.items))
|
||||
n.items.forEach(walk)
|
||||
// table nodes store content under header/rows/cells rather than children
|
||||
if (n.type === 'table') {
|
||||
if (n.header)
|
||||
walk(n.header)
|
||||
if (Array.isArray(n.rows))
|
||||
n.rows.forEach(walk)
|
||||
}
|
||||
else if (n.type === 'table_row') {
|
||||
if (Array.isArray(n.cells))
|
||||
n.cells.forEach(walk)
|
||||
}
|
||||
}
|
||||
;(nodes || []).forEach(walk)
|
||||
return out
|
||||
}
|
||||
|
||||
export function hasNode(nodes: any[], type: string) {
|
||||
return collect(nodes, type).length > 0
|
||||
}
|
||||
|
||||
export function fenceOrParagraph(nodes: any[]) {
|
||||
const fence = hasNode(nodes, 'code_block') || hasNode(nodes, 'fence')
|
||||
const para = hasNode(nodes, 'paragraph')
|
||||
return fence || para
|
||||
}
|
||||
|
||||
export function isParagraphOrArray(nodes: any[]) {
|
||||
return Array.isArray(nodes) || hasNode(nodes, 'paragraph')
|
||||
}
|
||||
|
||||
export function links(nodes: any[]) {
|
||||
return collect(nodes, 'link')
|
||||
}
|
||||
|
||||
export function hasLoadingLink(nodes: any[]) {
|
||||
const ls = links(nodes) || []
|
||||
return ls.some((l: any) => !!l.loading)
|
||||
}
|
||||
|
||||
export function textIncludes(nodes: any[], s: string | RegExp) {
|
||||
if (!s)
|
||||
return false
|
||||
const seen = new Set<any>()
|
||||
const walk = (n: any): boolean => {
|
||||
if (!n)
|
||||
return false
|
||||
// allow passing a raw string directly
|
||||
if (typeof n === 'string') {
|
||||
if (s instanceof RegExp)
|
||||
return s.test(n)
|
||||
return n.includes(s as string)
|
||||
}
|
||||
if (seen.has(n))
|
||||
return false
|
||||
seen.add(n)
|
||||
if (Array.isArray(n)) {
|
||||
for (const it of n) {
|
||||
if (walk(it))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// check common textual fields
|
||||
const checkFields = ['content', 'text', 'raw', 'href', 'src', 'title', 'name', 'markup']
|
||||
for (const f of checkFields) {
|
||||
if (typeof n[f] === 'string') {
|
||||
if (s instanceof RegExp) {
|
||||
if (s.test(n[f]))
|
||||
return true
|
||||
}
|
||||
else if (n[f].includes(s)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// also inspect attributes if present (array of [key,value])
|
||||
if (Array.isArray(n.attrs)) {
|
||||
for (const a of n.attrs) {
|
||||
if (Array.isArray(a) && a.length >= 2) {
|
||||
const v = String(a[1])
|
||||
if (s instanceof RegExp) {
|
||||
if (s.test(v))
|
||||
return true
|
||||
}
|
||||
else if (v.includes(s)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// recurse into children/items
|
||||
if (Array.isArray(n.children)) {
|
||||
for (const c of n.children) {
|
||||
if (walk(c))
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (Array.isArray(n.items)) {
|
||||
for (const it of n.items) {
|
||||
if (walk(it))
|
||||
return true
|
||||
}
|
||||
}
|
||||
// table content traversal
|
||||
if (n.type === 'table') {
|
||||
if (n.header && walk(n.header))
|
||||
return true
|
||||
if (Array.isArray(n.rows)) {
|
||||
for (const r of n.rows) {
|
||||
if (walk(r))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (n.type === 'table_row') {
|
||||
if (Array.isArray(n.cells)) {
|
||||
for (const c of n.cells) {
|
||||
if (walk(c))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return walk(nodes as any)
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function hasParagraph(nodes: any[]) {
|
||||
return hasNode(nodes, 'paragraph')
|
||||
}
|
||||
|
||||
export function hasEmoji(nodes: any[]) {
|
||||
return collect(nodes, 'emoji').length > 0
|
||||
}
|
||||
|
||||
export function paragraphFirst(nodes: any[]) {
|
||||
const ps = collect(nodes, 'paragraph')
|
||||
return ps.length > 0 ? ps[0] : null
|
||||
}
|
||||
|
||||
export function paragraphHasCheckbox(nodes: any[]) {
|
||||
const p = paragraphFirst(nodes)
|
||||
if (!p || !Array.isArray(p.children))
|
||||
return false
|
||||
return p.children.some((c: any) => c.type === 'checkbox' || c.type === 'checkbox_input')
|
||||
}
|
||||
Reference in New Issue
Block a user