2faeafaa by Adam Heath

Add some simple instructions.

1 parent f583ec04
.*.sw?
/node_modules/
/.astro/
......
./bin/run npm install
DOCKER_ARGS="-p 5555:4321" ./bin/run npm run dev -- --host
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'
import node from '@astrojs/node'
// https://astro.build/config
export default defineConfig({
integrations: [
react(),
],
adapter: node({
mode: 'middleware',
}),
output: 'server',
});
#!/bin/sh
set -e
TOP_DIR="$(cd "$(dirname "$0")/.."; pwd -P)"
ti_arg=""
if [ -t 0 ]; then
ti_arg="-ti"
fi
docker run --rm $ti_arg \
-e UID_REMAP=$(id -u) -e GID_REMAP=$(id -g) \
-v "$TOP_DIR/container-entrypoint.sh:/container-entrypoint.sh" --entrypoint /container-entrypoint.sh \
-w /srv/app \
-v "$TOP_DIR:/srv/app" \
$DOCKER_ARGS \
node "$@"
#!/bin/bash
set -e
if [[ 0 -eq $(id -u) ]]; then
[[ $UID_REMAP ]] && usermod -u $UID_REMAP node
[[ $GID_REMAP ]] && groupmod -g $GID_REMAP node
fi
if [[ $# -eq 0 ]]; then
set -- bash
fi
cmd="$1"
shift
start-stop-daemon -c node -d $PWD -u node --start --exec "$(which "$cmd")" -- "$@"
import { configureStore } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import { parseHtml } from './html.js'
export const configSlice = createSlice({
name: 'config',
initialState: {
sites: {},
},
reducers: {
setSiteConfig(state, { payload: { name, baseUrl } }) {
if (!state.sites[ name ]) state.sites[ name ] = {}
Object.assign(state.sites[ name ], { baseUrl })
}
},
selectors: {
getSiteBaseUrl: (state, name) => state.sites?.[ name ]?.baseUrl,
},
})
const baseQuery = fetchBaseQuery()
const siteBaseQuery = async (args, api, options) => {
const { site, url } = args
const baseUrl = getSiteBaseUrl(site)
return baseQuery({ ...args, url: `${baseUrl}/${url}` }, api, options)
}
export const sitePageSlice = createApi({
reducerPath: 'moqui',
tagTypes: ['Page'],
keepUnusedDataFor: 60,
refetchOnReconnect: true,
refetchOnMountOrArgChange: true,
baseQuery: siteBaseQuery,
endpoints: builder => ({
getPage: builder.query({
query: (args) => {
const { site, page } = args
return {
site,
url: page,
method: 'GET',
responseHandler: 'text',
}
},
providesTags: (result, err, args) => {
const { site, page } = args
return [ { type: 'Page', id: { site, page } } ]
},
}),
}),
})
export const store = configureStore({
reducer: {
[ configSlice.reducerPath ]: configSlice.reducer,
[ sitePageSlice.reducerPath ]: sitePageSlice.reducer,
},
middleware: getDefaultMiddleware => getDefaultMiddleware().concat([
sitePageSlice.middleware,
]),
})
export const getSitePage = async (site, page) => {
const result = await store.dispatch(sitePageSlice.endpoints.getPage.initiate({ site, page }))
if (result.data) result.doc = parseHtml(result.data)
return result
}
export const setSiteConfig = (siteDef) => {
return store.dispatch(configSlice.actions.setSiteConfig(siteDef))
}
export const getSiteBaseUrl = (name) => {
return configSlice.selectors.getSiteBaseUrl(store.getState(), name)
}
setSiteConfig({ name: 'msd', baseUrl: 'https://myspecialtydoc-d1d234.webflow.io' })
import Children from './children.astro'
import Custom from './custom.astro'
import Render from './render.astro'
import Replace from './replace.astro'
export { Children, Custom, Render, Replace }
import NodeCache from 'node-cache'
export const selectorCache = new NodeCache({
stdTTL: 10*60,
useClones: false,
})
export const parsedHtmlCache = new NodeCache({
stdTTL: 10*60,
useClones: false,
})
export const findNodeCache = new WeakMap()
---
import Render, { slotPassThrough } from './render.astro'
const { props: { debug, parent, children, replacers, slotHandler } } = Astro
const slotCallback = slotPassThrough(Astro)
//console.log('Children:render', { parent, children, replacers })
---
{
Array.isArray(children) ?
children.map((child, index) => <Render debug={debug} parent={parent} node={child} index={index} replacers={replacers} slotHandler={slotHandler}/>)
: !children ? ''
: <Render debug={debug} parent={parent} node={children} index={0} replacers={replacers} slotHandler={slotHandler}/>
}
---
import { slotPassThrough } from './render.astro'
import Children from './children.astro'
const { props: { wrap = false, wrapAttributes = {}, node, replacers, slotHandler } } = Astro
const { name: Name, attributes, children } = node
const CustomName = `custom-${Name}`
const slotCallback = slotPassThrough(Astro)
//console.log('Got custom match', node)
---
{
wrap ? (
<CustomName {...wrapAttributes}>
{
node.isSelfClosingTag ? <Name {...attributes}/>
: <Name {...attributes}>
<Children parent={node} children={children} replacers={replacers} slotHandler={slotHandler}/>
</Name>
}
</CustomName>
) : (
node.isSelfClosingTag ? <CustomName {...attributes}/>
: <CustomName {...attributes}>
<Children parent={node} children={children} replacers={replacers} slotHandler={slotHandler}/>
</CustomName>
)
}
import nodeUtil from 'util'
import NodeCache from 'node-cache'
import {
parse as umParse,
render as umRender,
transform as umTransform,
__unsafeHTML,
ELEMENT_NODE,
TEXT_NODE,
walkSync,
} from 'ultrahtml'
import {
parse as elParse,
specificity as getSpecificity,
specificityToNumber,
} from 'parsel-js'
import { parsedHtmlCache, selectorCache, findNodeCache } from './cache.js'
// TODO: implement a parent/child/element cache
const filterChildElements = (parent) => parent?.children?.filter(n => n.type === ELEMENT_NODE) || []
const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node);
const makeNthChildPosMatcher = (ast) => {
const { argument } = ast
const n = Number(argument)
if (!Number.isNaN(n)) {
return (context, node, parent, i) => {
return i === n
}
}
switch (argument) {
case 'odd':
return (context, node, parent, i) => Math.abs(i % 2) === 1
case 'even':
return (context, node, parent, i) => i % 2 === 0
default: {
if (!argument) throw new Error(`Unsupported empty nth-child selector!`)
let [_, A = '1', B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? []
if (A.length === 0) A = '1'
const a = Number.parseInt(A === '-' ? '-1' : A)
const b = Number.parseInt(B)
const nth = (index) => (a * n) + b
return (context, node, parent, i) => {
const elements = filterChildElements(parent)
for (let index = 0; index < elements.length; index++) {
const n = nth(index)
if (n > elements.length) return false
if (n === i) return true
}
return false
}
}
}
}
const getAttrValueMatch = (value, operator = '=', caseSenstive) => {
if (value === undefined) return (attrValue) => attrValue !== undefined
const isCaseInsenstive = caseSensitive === 'i'
if (isCaseInsensitive) value = value.toLowerCase()
const adjustMatcher = (matcher) => isCaseInsensitive ? (attrValue) => matcher(attrValue.toLowerCase()) : matcher
switch (operator) {
case '=': return (attrValue) => value === attrValue
case '~=': {
const keys = value.split(/\s+/g).reduce((keys, item) => {
keys[ item ] = true
return keys
}, {})
return adjustMatcher((attrValue) => keys[ attrValue ])
}
case '|=': return adjustMatcher((attrValue) => value.startsWith(attrValue + '-'))
case '*=': return adjustMatcher((attrValue) => value.indexOf(attrValue) > -1)
case '$=': return adjustMatcher((attrValue) => value.endsWith(attrValue))
case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue))
}
return (attrValue) => false
}
const compileMatcher = (ast, selector) => {
let counter = 0
const neededContext = []
const makeMatcher = (ast) => {
//console.log('makeMatcher', ast)
switch (ast.type) {
case 'list': {
const matchers = ast.list.map(s => makeMatcher(s))
return (context, node, parent, i, debug) => {
for (const matcher of matchers) {
if (!matcher(context, node, parent, i)) return false
}
return true
}
}
case 'compound': {
const matchers = ast.list.map(s => makeMatcher(s))
return (context, node, parent, i, debug) => {
for (const matcher of matchers) {
if (!matcher(context, node, parent, i)) return false
}
return true
}
}
case 'complex': {
const { left, right, combinator, pos } = ast
const leftMatcher = makeMatcher(left)
const rightMatcher = makeMatcher(right)
const setCounter = counter++
neededContext[ setCounter ] = () => new WeakSet()
return (context, node, parent, i, debug) => {
const seen = context[ setCounter ]
if (leftMatcher(context, node, parent, i, debug)) {
if (debug) console.log('matched on left', { left, right, combinator, pos, parent })
// TODO: Check seen.has(), and maybe skip calling leftMatcher?
seen.add(node)
} else if (parent && seen.has(parent) && combinator === ' ') {
seen.add(node)
}
if (!rightMatcher(context, node, parent, i, debug)) return false
seen.add(node)
if (debug) console.log('matched on right', { left, right, combinator, pos, node, parent })
switch (combinator) {
case ' ':
let parentPtr = parent
while (parentPtr) {
if (seen.has(parentPtr)) return true
parentPtr = parentPtr.parent
}
return false
case '>':
if (debug) console.log('seen parent', seen.has(parent))
return parent ? seen.has(parent) : false
case '+': {
if (!parent) return false
let prevSiblings = parent.children.slice(0, i).filter((el) => el.type === ELEMENT_NODE)
if (prevSiblings.length === 0) return false
const prev = prevSiblings[prevSiblings.length - 1]
if (!prev) return false
if (seen.has(prev)) return true
return false
}
case '~': {
if (!parent) return false
let prevSiblings = parent.children.slice(0, i).filter((el) => el.type === ELEMENT_NODE)
if (prevSiblings.length === 0) return false
for (const prev of prevSiblings) {
if (seen.has(prev)) return true
}
return false
}
default:
return false
}
}
}
case 'type': {
const { name, content } = ast
if (content === '*') return (context, node, parent, i) => true
return (context, node, parent, i, debug) => node.name === name
}
case 'class': {
const { name } = ast
return (context, node, parent, i, debug) => node.attributes?.['class']?.split(/\s+/g).includes(name)
}
case 'id': {
const { name } = ast
return (context, node, parent, i, debug) => node.attributes?.id === name
}
case 'pseudo-class':
switch (ast.name) {
case 'global':
return makeMatcher(elParse(ast.argument))
case 'not': {
const matcher = makeMatcher(ast.subtree)
return (...args) => !matcher(...args)
}
case 'is':
return makeMatcher(ast.subtree)
case 'where':
return makeMatcher(ast.subtree)
case 'root':
return (context, node, parent, i) => !node.parent
case 'empty':
return (context, node, parent, i, debug) => {
if (node.type !== ELEMENT_NODE) return false
const { children } = node
if (children.length === 0) return false
return children.every(child => child.type === TEXT_NODE && child.value.trim() === '')
}
case 'first-child':
return (context, node, parent, i, debug) => {
return parent?.children.findFirst(child => child.type === ELEMENT_NODE) === node
}
case 'last-child':
return (context, node, parent, i, debug) => {
return parent?.children.findLast(child => child.type === ELEMENT_NODE) === node
}
case 'only-child':
return (context, node, parent, i, debug) => {
// TODO: This can break-early after it finds the second element
return filterChildElements(parent).length === 1
}
case 'nth-child': {
const nthChildMatcher = makeNthChildPosMatcher(ast)
return (context, node, parent, i, debug) => {
const pos = nthChildPos(node, parent) + 1
return nthChildMatcher(context, node, parent, pos)
}
}
default:
throw new Error(`Unknown pseudo-class: ${ast.name}`)
}
case 'attribute':
const { caseSensitive, name, value, operator } = ast
const attrValueMatch = getAttrValueMatch(value, operator, caseSenstive)
return (context, node, parent, i, debug) => {
const { attributes: { [ name ]: attrValue } = {} } = node
return attrValueMatch(attrValue)
}
case 'universal':
return (context, node, parent, i, debug) => true
default:
throw new Error(`Unhandled ast: ${ast.type}`)
}
}
const matcher = makeMatcher(ast)
return () => {
const context = neededContext.map(item => item())
const nodeMatcher = (node, parent, i, debug) => {
//if (debug) console.log('starting to match', {node, context})
return matcher(context, node, parent, i, debug)
}
nodeMatcher.toString = () => {
return '[matcher:' + selector + ']'
}
return nodeMatcher
}
}
export const createMatcher = (selector) => {
const matcherCreater = selectorCache.get(selector)
if (false && matcher) return matcherCreater()
const ast = elParse(selector)
//console.log('createMatcher', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
const newMatcherCreater = compileMatcher(ast, selector)
selectorCache.set(selector, newMatcherCreater)
return newMatcherCreater()
}
export const parseHtml = (html) => {
const cached = parsedHtmlCache.get(html)
if (cached) return cached
const doc = umParse(html)
parsedHtmlCache.set(html, doc)
return doc
}
export const findNode = (doc, selector) => {
if (!selector) return doc
let docCache = findNodeCache.get(doc)
if (!docCache) {
docCache = new NodeCache({ stdTTL: 10*60, useClones: false })
findNodeCache.set(doc, docCache)
}
const found = docCache.get(selector)
if (found !== undefined) return found[0]
//console.log('cache miss', {selector})
const matcher = createMatcher(selector)
try {
walkSync(doc, (node, parent, index) => {
if (matcher(node, parent, index)) throw node
})
} catch (e) {
if (e instanceof Error) throw e
docCache.set(selector, [ e ])
return e
}
}
---
import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
import Children from './children.astro'
export const slotPassThrough = (Astro) => (slotName, node) => {
console.log('calling slot', { slotName, node, Astro })
return Astro.slots.render('default', [ slotName, node ])
}
const { props: { debug = false, parent = null, node, index = 0, replacers, slotHandler } } = Astro
const { name: Name, attributes } = node
if (debug) {
console.log('trying to match against', {node})
}
let slotName
for (const [ matcher, handler ] of replacers) {
if (debug) console.log('attempting matcher', matcher.toString())
if (matcher(node, parent, index, false)) {
if (debug) console.log('matched')
slotName = handler
}
}
//const slotCallback = slotPassThrough(Astro)
---
{
slotName ? slotHandler(slotName, node)
: node.type === TEXT_NODE ? <Fragment set:html={node.value}/>
: node.type === ELEMENT_NODE ? (
node.isSelfClosingTag ? <Name {...attributes}/>
: <Name {...attributes}>
<Children parent={node} children={node.children} replacers={replacers} slotHandler={slotHandler}/>
</Name>
) : (
<Children parent={node} children={node.children} replacers={replacers} slotHandler={slotHandler}/>
)
}
---
import html from '@resources/provider-portal.html?raw'
import { walkSync } from 'ultrahtml'
import { parseHtml, createMatcher, findNode } from './html.js'
import Render from './render.astro'
const { props } = Astro
const { html, mapClassname = true, xpath, replacements = {} } = props
const doc = props.node ? props.node : parseHtml(props.html)
const node = xpath ? findNode(doc, xpath) : doc
const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ])
const fixAttributes = ({ attributes }) => {
if (!mapClassname) return attributes
const { class: className, ...rest } = attributes
return { ...rest, className }
}
const slotHandler = (slotName, node) => {
const attributes = fixAttributes(node)
return Astro.slots.render(slotName, [ { ...node, attributes }, { slotHandler, replacers } ] )
}
---
<Render node={node} replacers={replacers} slotHandler={slotHandler}/>
This diff could not be displayed because it is too large.
{
"name": "astro-wt",
"type": "module",
"exports": {
"./astro": "./src/astro.mjs",
"./client": "./src/client.mjs",
"./slices": "./src/slices.mjs",
"./react": "./src/react.jsx",
"./session": "./src/session.mjs",
"./ReduxIsland": "./src/ReduxIsland.astro"
},
"scripts": {
"dev": "astro dev"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.5",
"node-cache": "^5.1.2",
"parsel-js": "^1.1.2",
"ultrahtml": "^1.5.3"
},
"devDependencies": {
"@astrojs/node": "^8.2.5",
"@astrojs/react": "^3.3.4",
"astro": "^4.8.7",
"react": "^18.3.1"
}
}
import React from 'react'
export const Demographics = (props) => {
return 'demographics'
}
import { getSitePage, setSiteConfig } from '@lib/api.ts'
setSiteConfig({ name: 'msd', baseUrl: 'https://myspecialtydoc-d1d234.webflow.io' })
/// <reference types="astro/client" />
\ No newline at end of file
---
import { getSitePage } from '@lib/api.ts'
import { Replace } from '@lib/astro.ts'
import '@config.ts'
const { data: layoutHtml } = await getSitePage('msd', '/')
const layoutReplacements = {
['.hero-section']: 'content',
['.content-section']: 'DELETE',
['.content-section-4']: 'DELETE',
//['.navbar.w-nav:nth-child(n + 1)']: 'DELETE',
// The following 2 are not implemented yet
//[':nth-child(n + 1 of .navbar.w-nav)']: 'DELETE',
//['.navbar.w-nav:nth-of-type(n + 1)']: 'DELETE',
}
---
<Replace debug={true} html={layoutHtml} replacements={layoutReplacements}>
<define slot="DELETE">{(node) => ''}</define>
<define slot='content'>{(node) => <slot/>}</define>
</Replace>
---
import { getSitePage } from '@lib/api.ts'
import { Replace } from '@lib/astro.ts'
import BaseLayout from '@layouts/base.astro'
import { Demographics } from '@components/EventDetails.jsx'
const { data: layoutHtml } = await getSitePage('msd', '/')
const { data: pageHtml } = await getSitePage('msd', '/provider-portal')
const pageReplacements = {
['#w-node-_3615b991-cc00-f776-58c2-a728a0fba1a9-70cc1f5d']: 'Demographics',
['#w-node-eaef93e0-275c-ff2c-39b3-0cc9b81495f3-70cc1f5d .div-block-11']: 'DELETE',
['#w-node-eaef93e0-275c-ff2c-39b3-0cc9b81495f3-70cc1f5d > .div-block-8 img']: 'custom',
['#w-node-eaef93e0-275c-ff2c-39b3-0cc9b81495f3-70cc1f5d > .div-block-8']: 'Comms',
['#w-node-a9641a71-b768-107f-2209-4491bf21ed0a-70cc1f5d > .textblock']: 'ChiefComplaint',
['#w-node-bbdf8d21-dc38-5a20-e2b8-38fbe2e0f88a-70cc1f5d > .textblock']: 'HealthHistory',
['#w-node-_4b7383d7-9725-37cd-1d15-d6292436eb85-70cc1f5d .textblock']: 'Subjective',
// ['#w-node-a97d0be9-a1a9-af1d-a30b-f54664c79b09-70cc1f5d > .textblock']: 'ObjectiveLevels',
['#w-node-_8c10d845-8d0d-25e6-d1da-42fc4c0faa17-70cc1f5d > .textblock']: 'Objective',
['#w-node-_77cf40d0-cb39-d173-d0ed-0a30bcbbda03-70cc1f5d ~ .div-block-15 > .textblock']: 'Assessment',
['#w-node-f55c4197-553d-142d-be0f-1364e3d5ce27-70cc1f5d .textblock']: 'Assessment',
['#w-node-_993bee43-3117-30d9-0b03-cb9d32541bb9-70cc1f5d > .textblock']: 'Formulary',
['#w-node-_1817664f-14e2-687e-1460-429ef0a62ab3-70cc1f5d .textblock']: 'Plan',
}
---
<BaseLayout>
<Replace html={pageHtml} xpath='.container-2.w-container' replacements={pageReplacements}>
<define slot='DELETE'>{(node, context) => ''}</define>
<define slot='Demographics'>{(node, context) => <Demographics/>}</define>
</Replace>
</BaseLayout>
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/*"],
"@*": ["src/*"]
}
}
}