2faeafaa by Adam Heath

Add some simple instructions.

1 parent f583ec04
1 .*.sw? 1 .*.sw?
2
3 /node_modules/
4 /.astro/
......
1
2 ./bin/run npm install
3 DOCKER_ARGS="-p 5555:4321" ./bin/run npm run dev -- --host
1 import { defineConfig } from 'astro/config'
2 import react from '@astrojs/react'
3 import node from '@astrojs/node'
4
5 // https://astro.build/config
6 export default defineConfig({
7 integrations: [
8 react(),
9 ],
10 adapter: node({
11 mode: 'middleware',
12 }),
13 output: 'server',
14 });
15
1 #!/bin/sh
2
3 set -e
4
5 TOP_DIR="$(cd "$(dirname "$0")/.."; pwd -P)"
6
7 ti_arg=""
8 if [ -t 0 ]; then
9 ti_arg="-ti"
10 fi
11 docker run --rm $ti_arg \
12 -e UID_REMAP=$(id -u) -e GID_REMAP=$(id -g) \
13 -v "$TOP_DIR/container-entrypoint.sh:/container-entrypoint.sh" --entrypoint /container-entrypoint.sh \
14 -w /srv/app \
15 -v "$TOP_DIR:/srv/app" \
16 $DOCKER_ARGS \
17 node "$@"
18
1 #!/bin/bash
2
3 set -e
4 if [[ 0 -eq $(id -u) ]]; then
5 [[ $UID_REMAP ]] && usermod -u $UID_REMAP node
6 [[ $GID_REMAP ]] && groupmod -g $GID_REMAP node
7 fi
8 if [[ $# -eq 0 ]]; then
9 set -- bash
10 fi
11
12 cmd="$1"
13 shift
14 start-stop-daemon -c node -d $PWD -u node --start --exec "$(which "$cmd")" -- "$@"
1 import { configureStore } from '@reduxjs/toolkit'
2 import { createSlice } from '@reduxjs/toolkit'
3 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
4
5 import { parseHtml } from './html.js'
6
7 export const configSlice = createSlice({
8 name: 'config',
9 initialState: {
10 sites: {},
11 },
12 reducers: {
13 setSiteConfig(state, { payload: { name, baseUrl } }) {
14 if (!state.sites[ name ]) state.sites[ name ] = {}
15 Object.assign(state.sites[ name ], { baseUrl })
16 }
17 },
18 selectors: {
19 getSiteBaseUrl: (state, name) => state.sites?.[ name ]?.baseUrl,
20 },
21 })
22
23
24 const baseQuery = fetchBaseQuery()
25
26 const siteBaseQuery = async (args, api, options) => {
27 const { site, url } = args
28 const baseUrl = getSiteBaseUrl(site)
29 return baseQuery({ ...args, url: `${baseUrl}/${url}` }, api, options)
30 }
31
32 export const sitePageSlice = createApi({
33 reducerPath: 'moqui',
34 tagTypes: ['Page'],
35 keepUnusedDataFor: 60,
36 refetchOnReconnect: true,
37 refetchOnMountOrArgChange: true,
38 baseQuery: siteBaseQuery,
39 endpoints: builder => ({
40 getPage: builder.query({
41 query: (args) => {
42 const { site, page } = args
43 return {
44 site,
45 url: page,
46 method: 'GET',
47 responseHandler: 'text',
48 }
49 },
50 providesTags: (result, err, args) => {
51 const { site, page } = args
52 return [ { type: 'Page', id: { site, page } } ]
53 },
54 }),
55 }),
56 })
57
58 export const store = configureStore({
59 reducer: {
60 [ configSlice.reducerPath ]: configSlice.reducer,
61 [ sitePageSlice.reducerPath ]: sitePageSlice.reducer,
62 },
63 middleware: getDefaultMiddleware => getDefaultMiddleware().concat([
64 sitePageSlice.middleware,
65 ]),
66 })
67
68 export const getSitePage = async (site, page) => {
69 const result = await store.dispatch(sitePageSlice.endpoints.getPage.initiate({ site, page }))
70 if (result.data) result.doc = parseHtml(result.data)
71 return result
72 }
73
74 export const setSiteConfig = (siteDef) => {
75 return store.dispatch(configSlice.actions.setSiteConfig(siteDef))
76 }
77
78 export const getSiteBaseUrl = (name) => {
79 return configSlice.selectors.getSiteBaseUrl(store.getState(), name)
80 }
81
82 setSiteConfig({ name: 'msd', baseUrl: 'https://myspecialtydoc-d1d234.webflow.io' })
83
1 import Children from './children.astro'
2 import Custom from './custom.astro'
3 import Render from './render.astro'
4 import Replace from './replace.astro'
5
6 export { Children, Custom, Render, Replace }
1 import NodeCache from 'node-cache'
2
3 export const selectorCache = new NodeCache({
4 stdTTL: 10*60,
5 useClones: false,
6 })
7
8 export const parsedHtmlCache = new NodeCache({
9 stdTTL: 10*60,
10 useClones: false,
11 })
12
13 export const findNodeCache = new WeakMap()
1 ---
2 import Render, { slotPassThrough } from './render.astro'
3
4 const { props: { debug, parent, children, replacers, slotHandler } } = Astro
5
6 const slotCallback = slotPassThrough(Astro)
7
8 //console.log('Children:render', { parent, children, replacers })
9 ---
10 {
11 Array.isArray(children) ?
12 children.map((child, index) => <Render debug={debug} parent={parent} node={child} index={index} replacers={replacers} slotHandler={slotHandler}/>)
13 : !children ? ''
14 : <Render debug={debug} parent={parent} node={children} index={0} replacers={replacers} slotHandler={slotHandler}/>
15 }
1 ---
2
3 import { slotPassThrough } from './render.astro'
4 import Children from './children.astro'
5
6 const { props: { wrap = false, wrapAttributes = {}, node, replacers, slotHandler } } = Astro
7 const { name: Name, attributes, children } = node
8
9 const CustomName = `custom-${Name}`
10 const slotCallback = slotPassThrough(Astro)
11
12 //console.log('Got custom match', node)
13 ---
14 {
15 wrap ? (
16 <CustomName {...wrapAttributes}>
17 {
18 node.isSelfClosingTag ? <Name {...attributes}/>
19 : <Name {...attributes}>
20 <Children parent={node} children={children} replacers={replacers} slotHandler={slotHandler}/>
21 </Name>
22 }
23 </CustomName>
24 ) : (
25 node.isSelfClosingTag ? <CustomName {...attributes}/>
26 : <CustomName {...attributes}>
27 <Children parent={node} children={children} replacers={replacers} slotHandler={slotHandler}/>
28 </CustomName>
29 )
30 }
31
1 import nodeUtil from 'util'
2 import NodeCache from 'node-cache'
3
4 import {
5 parse as umParse,
6 render as umRender,
7 transform as umTransform,
8 __unsafeHTML,
9 ELEMENT_NODE,
10 TEXT_NODE,
11 walkSync,
12 } from 'ultrahtml'
13
14 import {
15 parse as elParse,
16 specificity as getSpecificity,
17 specificityToNumber,
18 } from 'parsel-js'
19
20 import { parsedHtmlCache, selectorCache, findNodeCache } from './cache.js'
21
22 // TODO: implement a parent/child/element cache
23 const filterChildElements = (parent) => parent?.children?.filter(n => n.type === ELEMENT_NODE) || []
24 const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node);
25
26 const makeNthChildPosMatcher = (ast) => {
27 const { argument } = ast
28 const n = Number(argument)
29 if (!Number.isNaN(n)) {
30 return (context, node, parent, i) => {
31 return i === n
32 }
33 }
34 switch (argument) {
35 case 'odd':
36 return (context, node, parent, i) => Math.abs(i % 2) === 1
37 case 'even':
38 return (context, node, parent, i) => i % 2 === 0
39 default: {
40 if (!argument) throw new Error(`Unsupported empty nth-child selector!`)
41 let [_, A = '1', B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? []
42 if (A.length === 0) A = '1'
43 const a = Number.parseInt(A === '-' ? '-1' : A)
44 const b = Number.parseInt(B)
45 const nth = (index) => (a * n) + b
46 return (context, node, parent, i) => {
47 const elements = filterChildElements(parent)
48 for (let index = 0; index < elements.length; index++) {
49 const n = nth(index)
50 if (n > elements.length) return false
51 if (n === i) return true
52 }
53 return false
54 }
55 }
56 }
57 }
58
59 const getAttrValueMatch = (value, operator = '=', caseSenstive) => {
60 if (value === undefined) return (attrValue) => attrValue !== undefined
61 const isCaseInsenstive = caseSensitive === 'i'
62 if (isCaseInsensitive) value = value.toLowerCase()
63 const adjustMatcher = (matcher) => isCaseInsensitive ? (attrValue) => matcher(attrValue.toLowerCase()) : matcher
64 switch (operator) {
65 case '=': return (attrValue) => value === attrValue
66 case '~=': {
67 const keys = value.split(/\s+/g).reduce((keys, item) => {
68 keys[ item ] = true
69 return keys
70 }, {})
71 return adjustMatcher((attrValue) => keys[ attrValue ])
72 }
73 case '|=': return adjustMatcher((attrValue) => value.startsWith(attrValue + '-'))
74 case '*=': return adjustMatcher((attrValue) => value.indexOf(attrValue) > -1)
75 case '$=': return adjustMatcher((attrValue) => value.endsWith(attrValue))
76 case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue))
77 }
78 return (attrValue) => false
79 }
80
81 const compileMatcher = (ast, selector) => {
82 let counter = 0
83
84 const neededContext = []
85 const makeMatcher = (ast) => {
86 //console.log('makeMatcher', ast)
87 switch (ast.type) {
88 case 'list': {
89 const matchers = ast.list.map(s => makeMatcher(s))
90 return (context, node, parent, i, debug) => {
91 for (const matcher of matchers) {
92 if (!matcher(context, node, parent, i)) return false
93 }
94 return true
95 }
96 }
97 case 'compound': {
98 const matchers = ast.list.map(s => makeMatcher(s))
99 return (context, node, parent, i, debug) => {
100 for (const matcher of matchers) {
101 if (!matcher(context, node, parent, i)) return false
102 }
103 return true
104 }
105 }
106 case 'complex': {
107 const { left, right, combinator, pos } = ast
108 const leftMatcher = makeMatcher(left)
109 const rightMatcher = makeMatcher(right)
110 const setCounter = counter++
111 neededContext[ setCounter ] = () => new WeakSet()
112 return (context, node, parent, i, debug) => {
113 const seen = context[ setCounter ]
114 if (leftMatcher(context, node, parent, i, debug)) {
115 if (debug) console.log('matched on left', { left, right, combinator, pos, parent })
116 // TODO: Check seen.has(), and maybe skip calling leftMatcher?
117 seen.add(node)
118 } else if (parent && seen.has(parent) && combinator === ' ') {
119 seen.add(node)
120 }
121 if (!rightMatcher(context, node, parent, i, debug)) return false
122 seen.add(node)
123 if (debug) console.log('matched on right', { left, right, combinator, pos, node, parent })
124 switch (combinator) {
125 case ' ':
126 let parentPtr = parent
127 while (parentPtr) {
128 if (seen.has(parentPtr)) return true
129 parentPtr = parentPtr.parent
130 }
131 return false
132 case '>':
133 if (debug) console.log('seen parent', seen.has(parent))
134 return parent ? seen.has(parent) : false
135 case '+': {
136 if (!parent) return false
137 let prevSiblings = parent.children.slice(0, i).filter((el) => el.type === ELEMENT_NODE)
138 if (prevSiblings.length === 0) return false
139 const prev = prevSiblings[prevSiblings.length - 1]
140 if (!prev) return false
141 if (seen.has(prev)) return true
142 return false
143 }
144 case '~': {
145 if (!parent) return false
146 let prevSiblings = parent.children.slice(0, i).filter((el) => el.type === ELEMENT_NODE)
147 if (prevSiblings.length === 0) return false
148 for (const prev of prevSiblings) {
149 if (seen.has(prev)) return true
150 }
151 return false
152 }
153 default:
154 return false
155 }
156 }
157 }
158 case 'type': {
159 const { name, content } = ast
160 if (content === '*') return (context, node, parent, i) => true
161 return (context, node, parent, i, debug) => node.name === name
162 }
163 case 'class': {
164 const { name } = ast
165 return (context, node, parent, i, debug) => node.attributes?.['class']?.split(/\s+/g).includes(name)
166 }
167 case 'id': {
168 const { name } = ast
169 return (context, node, parent, i, debug) => node.attributes?.id === name
170 }
171 case 'pseudo-class':
172 switch (ast.name) {
173 case 'global':
174 return makeMatcher(elParse(ast.argument))
175 case 'not': {
176 const matcher = makeMatcher(ast.subtree)
177 return (...args) => !matcher(...args)
178 }
179 case 'is':
180 return makeMatcher(ast.subtree)
181 case 'where':
182 return makeMatcher(ast.subtree)
183 case 'root':
184 return (context, node, parent, i) => !node.parent
185 case 'empty':
186 return (context, node, parent, i, debug) => {
187 if (node.type !== ELEMENT_NODE) return false
188 const { children } = node
189 if (children.length === 0) return false
190 return children.every(child => child.type === TEXT_NODE && child.value.trim() === '')
191 }
192 case 'first-child':
193 return (context, node, parent, i, debug) => {
194 return parent?.children.findFirst(child => child.type === ELEMENT_NODE) === node
195 }
196 case 'last-child':
197 return (context, node, parent, i, debug) => {
198 return parent?.children.findLast(child => child.type === ELEMENT_NODE) === node
199 }
200 case 'only-child':
201 return (context, node, parent, i, debug) => {
202 // TODO: This can break-early after it finds the second element
203 return filterChildElements(parent).length === 1
204 }
205 case 'nth-child': {
206 const nthChildMatcher = makeNthChildPosMatcher(ast)
207 return (context, node, parent, i, debug) => {
208 const pos = nthChildPos(node, parent) + 1
209 return nthChildMatcher(context, node, parent, pos)
210 }
211 }
212 default:
213 throw new Error(`Unknown pseudo-class: ${ast.name}`)
214 }
215 case 'attribute':
216 const { caseSensitive, name, value, operator } = ast
217 const attrValueMatch = getAttrValueMatch(value, operator, caseSenstive)
218 return (context, node, parent, i, debug) => {
219 const { attributes: { [ name ]: attrValue } = {} } = node
220 return attrValueMatch(attrValue)
221 }
222 case 'universal':
223 return (context, node, parent, i, debug) => true
224 default:
225 throw new Error(`Unhandled ast: ${ast.type}`)
226 }
227 }
228 const matcher = makeMatcher(ast)
229 return () => {
230 const context = neededContext.map(item => item())
231 const nodeMatcher = (node, parent, i, debug) => {
232 //if (debug) console.log('starting to match', {node, context})
233 return matcher(context, node, parent, i, debug)
234 }
235 nodeMatcher.toString = () => {
236 return '[matcher:' + selector + ']'
237 }
238 return nodeMatcher
239 }
240 }
241
242 export const createMatcher = (selector) => {
243 const matcherCreater = selectorCache.get(selector)
244 if (false && matcher) return matcherCreater()
245 const ast = elParse(selector)
246 //console.log('createMatcher', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
247 const newMatcherCreater = compileMatcher(ast, selector)
248 selectorCache.set(selector, newMatcherCreater)
249 return newMatcherCreater()
250 }
251
252 export const parseHtml = (html) => {
253 const cached = parsedHtmlCache.get(html)
254 if (cached) return cached
255 const doc = umParse(html)
256 parsedHtmlCache.set(html, doc)
257 return doc
258 }
259
260 export const findNode = (doc, selector) => {
261 if (!selector) return doc
262 let docCache = findNodeCache.get(doc)
263 if (!docCache) {
264 docCache = new NodeCache({ stdTTL: 10*60, useClones: false })
265 findNodeCache.set(doc, docCache)
266 }
267 const found = docCache.get(selector)
268 if (found !== undefined) return found[0]
269 //console.log('cache miss', {selector})
270 const matcher = createMatcher(selector)
271 try {
272 walkSync(doc, (node, parent, index) => {
273 if (matcher(node, parent, index)) throw node
274 })
275 } catch (e) {
276 if (e instanceof Error) throw e
277 docCache.set(selector, [ e ])
278 return e
279 }
280 }
1 ---
2 import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
3 import Children from './children.astro'
4
5 export const slotPassThrough = (Astro) => (slotName, node) => {
6 console.log('calling slot', { slotName, node, Astro })
7 return Astro.slots.render('default', [ slotName, node ])
8 }
9
10 const { props: { debug = false, parent = null, node, index = 0, replacers, slotHandler } } = Astro
11 const { name: Name, attributes } = node
12
13 if (debug) {
14 console.log('trying to match against', {node})
15 }
16 let slotName
17 for (const [ matcher, handler ] of replacers) {
18 if (debug) console.log('attempting matcher', matcher.toString())
19 if (matcher(node, parent, index, false)) {
20 if (debug) console.log('matched')
21 slotName = handler
22 }
23 }
24
25 //const slotCallback = slotPassThrough(Astro)
26 ---
27 {
28 slotName ? slotHandler(slotName, node)
29 : node.type === TEXT_NODE ? <Fragment set:html={node.value}/>
30 : node.type === ELEMENT_NODE ? (
31 node.isSelfClosingTag ? <Name {...attributes}/>
32 : <Name {...attributes}>
33 <Children parent={node} children={node.children} replacers={replacers} slotHandler={slotHandler}/>
34 </Name>
35 ) : (
36 <Children parent={node} children={node.children} replacers={replacers} slotHandler={slotHandler}/>
37 )
38 }
1 ---
2 import html from '@resources/provider-portal.html?raw'
3 import { walkSync } from 'ultrahtml'
4 import { parseHtml, createMatcher, findNode } from './html.js'
5 import Render from './render.astro'
6
7 const { props } = Astro
8 const { html, mapClassname = true, xpath, replacements = {} } = props
9 const doc = props.node ? props.node : parseHtml(props.html)
10
11 const node = xpath ? findNode(doc, xpath) : doc
12 const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ])
13 const fixAttributes = ({ attributes }) => {
14 if (!mapClassname) return attributes
15 const { class: className, ...rest } = attributes
16 return { ...rest, className }
17 }
18
19 const slotHandler = (slotName, node) => {
20 const attributes = fixAttributes(node)
21 return Astro.slots.render(slotName, [ { ...node, attributes }, { slotHandler, replacers } ] )
22 }
23 ---
24 <Render node={node} replacers={replacers} slotHandler={slotHandler}/>
This diff could not be displayed because it is too large.
1 {
2 "name": "astro-wt",
3 "type": "module",
4 "exports": {
5 "./astro": "./src/astro.mjs",
6 "./client": "./src/client.mjs",
7 "./slices": "./src/slices.mjs",
8 "./react": "./src/react.jsx",
9 "./session": "./src/session.mjs",
10 "./ReduxIsland": "./src/ReduxIsland.astro"
11 },
12 "scripts": {
13 "dev": "astro dev"
14 },
15 "dependencies": {
16 "@reduxjs/toolkit": "^2.2.5",
17 "node-cache": "^5.1.2",
18 "parsel-js": "^1.1.2",
19 "ultrahtml": "^1.5.3"
20 },
21 "devDependencies": {
22 "@astrojs/node": "^8.2.5",
23 "@astrojs/react": "^3.3.4",
24 "astro": "^4.8.7",
25 "react": "^18.3.1"
26 }
27 }
1 import React from 'react'
2
3
4 export const Demographics = (props) => {
5 return 'demographics'
6 }
1 import { getSitePage, setSiteConfig } from '@lib/api.ts'
2
3 setSiteConfig({ name: 'msd', baseUrl: 'https://myspecialtydoc-d1d234.webflow.io' })
4
1 /// <reference types="astro/client" />
...\ No newline at end of file ...\ No newline at end of file
1 ---
2 import { getSitePage } from '@lib/api.ts'
3 import { Replace } from '@lib/astro.ts'
4 import '@config.ts'
5
6 const { data: layoutHtml } = await getSitePage('msd', '/')
7 const layoutReplacements = {
8 ['.hero-section']: 'content',
9 ['.content-section']: 'DELETE',
10 ['.content-section-4']: 'DELETE',
11 //['.navbar.w-nav:nth-child(n + 1)']: 'DELETE',
12 // The following 2 are not implemented yet
13 //[':nth-child(n + 1 of .navbar.w-nav)']: 'DELETE',
14 //['.navbar.w-nav:nth-of-type(n + 1)']: 'DELETE',
15 }
16 ---
17 <Replace debug={true} html={layoutHtml} replacements={layoutReplacements}>
18 <define slot="DELETE">{(node) => ''}</define>
19 <define slot='content'>{(node) => <slot/>}</define>
20 </Replace>
1 ---
2 import { getSitePage } from '@lib/api.ts'
3 import { Replace } from '@lib/astro.ts'
4 import BaseLayout from '@layouts/base.astro'
5 import { Demographics } from '@components/EventDetails.jsx'
6
7 const { data: layoutHtml } = await getSitePage('msd', '/')
8 const { data: pageHtml } = await getSitePage('msd', '/provider-portal')
9 const pageReplacements = {
10 ['#w-node-_3615b991-cc00-f776-58c2-a728a0fba1a9-70cc1f5d']: 'Demographics',
11 ['#w-node-eaef93e0-275c-ff2c-39b3-0cc9b81495f3-70cc1f5d .div-block-11']: 'DELETE',
12 ['#w-node-eaef93e0-275c-ff2c-39b3-0cc9b81495f3-70cc1f5d > .div-block-8 img']: 'custom',
13 ['#w-node-eaef93e0-275c-ff2c-39b3-0cc9b81495f3-70cc1f5d > .div-block-8']: 'Comms',
14 ['#w-node-a9641a71-b768-107f-2209-4491bf21ed0a-70cc1f5d > .textblock']: 'ChiefComplaint',
15 ['#w-node-bbdf8d21-dc38-5a20-e2b8-38fbe2e0f88a-70cc1f5d > .textblock']: 'HealthHistory',
16 ['#w-node-_4b7383d7-9725-37cd-1d15-d6292436eb85-70cc1f5d .textblock']: 'Subjective',
17 // ['#w-node-a97d0be9-a1a9-af1d-a30b-f54664c79b09-70cc1f5d > .textblock']: 'ObjectiveLevels',
18 ['#w-node-_8c10d845-8d0d-25e6-d1da-42fc4c0faa17-70cc1f5d > .textblock']: 'Objective',
19 ['#w-node-_77cf40d0-cb39-d173-d0ed-0a30bcbbda03-70cc1f5d ~ .div-block-15 > .textblock']: 'Assessment',
20 ['#w-node-f55c4197-553d-142d-be0f-1364e3d5ce27-70cc1f5d .textblock']: 'Assessment',
21 ['#w-node-_993bee43-3117-30d9-0b03-cb9d32541bb9-70cc1f5d > .textblock']: 'Formulary',
22 ['#w-node-_1817664f-14e2-687e-1460-429ef0a62ab3-70cc1f5d .textblock']: 'Plan',
23 }
24 ---
25 <BaseLayout>
26 <Replace html={pageHtml} xpath='.container-2.w-container' replacements={pageReplacements}>
27 <define slot='DELETE'>{(node, context) => ''}</define>
28 <define slot='Demographics'>{(node, context) => <Demographics/>}</define>
29 </Replace>
30 </BaseLayout>
1 {
2 "extends": "astro/tsconfigs/base",
3 "compilerOptions": {
4 "baseUrl": ".",
5 "paths": {
6 "@lib/*": ["lib/*"],
7 "@*": ["src/*"]
8 }
9 }
10 }
11