Initial commit: Korrith website, assessment API, PocketBase schema, Docker/Coolify deployment

This commit is contained in:
Korrith
2026-09-17 20:47:51 +00:00
commit c17f11f591
248 changed files with 39721 additions and 0 deletions
@@ -0,0 +1,301 @@
import * as t from '@babel/types';
import { ELEMENT_TYPE_MAP } from '../constants/selectors.js';
export const EDITABLE_HTML_TAGS = ["a", "Link", "button", "Button", "p", "span", "time", "h1", "h2", "h3", "h4", "h5", "h6", "label", "Label", "img"];
export const INLINE_EDIT_HTML_TAGS = ["br", "strong", "em", "b", "i", "u", "span", "a"];
/**
* @param {import('@babel/types').JSXOpeningElement} openingElementNode
* @param {string[]} editableTagsList
* @returns {boolean}
*/
export function checkTagNameEditable(openingElementNode, editableTagsList) {
if (!openingElementNode || !openingElementNode.name) return false;
const nameNode = openingElementNode.name;
if (nameNode.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.name)) {
return true;
}
if (nameNode.type === 'JSXMemberExpression' && nameNode.property && nameNode.property.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.property.name)) {
return true;
}
return false;
}
/**
* @param {import('@babel/types').JSXOpeningElement} openingNode
* @returns {{ isValid: boolean, reason: string|null }}
*/
export function validateImageSrc(openingNode) {
if (!openingNode || !openingNode.name || (openingNode.name.name !== 'img' && openingNode.name.property?.name !== 'img')) {
return { isValid: true, reason: null };
}
const hasPropsSpread = openingNode.attributes.some(attribute =>
t.isJSXSpreadAttribute(attribute) &&
attribute.argument &&
t.isIdentifier(attribute.argument) &&
attribute.argument.name === 'props'
);
if (hasPropsSpread) return { isValid: false, reason: 'props-spread' };
const sourceAttribute = openingNode.attributes.find(attribute =>
t.isJSXAttribute(attribute) && attribute.name && attribute.name.name === 'src'
);
if (!sourceAttribute) return { isValid: false, reason: 'missing-src' };
if (!t.isStringLiteral(sourceAttribute.value)) return { isValid: false, reason: 'dynamic-src' };
if (!sourceAttribute.value.value || sourceAttribute.value.value.trim() === '') return { isValid: false, reason: 'empty-src' };
return { isValid: true, reason: null };
}
// `children` and capitalised identifiers hold rendered nodes, not text, so an
// element whose only content is one of those is a layout slot rather than a target.
function isNodeSlotExpression(expression) {
if (t.isIdentifier(expression)) return expression.name === 'children' || /^[A-Z]/.test(expression.name);
return t.isMemberExpression(expression) && expression.property?.name === 'children';
}
function isLikelyTextExpression(expression) {
if (isNodeSlotExpression(expression)) return false;
// A ternary or `&&` still renders text through whichever branch wins. Without
// this, `{ready ? `${n} entries` : 'Loading'}` reads as an empty element and
// the span is skipped entirely instead of being marked assisted.
if (t.isConditionalExpression(expression)) {
return isLikelyTextExpression(expression.consequent) || isLikelyTextExpression(expression.alternate);
}
if (t.isLogicalExpression(expression)) {
return isLikelyTextExpression(expression.right);
}
return t.isIdentifier(expression)
|| t.isMemberExpression(expression)
|| t.isStringLiteral(expression)
|| t.isNumericLiteral(expression)
|| t.isTemplateLiteral(expression);
}
/**
* True when an element has direct text children worth editing
* @param {import('@babel/types').JSXElement} elementNode
* @returns {boolean}
*/
export function hasMeaningfulTextContent(elementNode) {
if (!t.isJSXElement(elementNode) || !elementNode.children?.length) return false;
for (const child of elementNode.children) {
if (t.isJSXText(child) && child.value.trim().length > 0) return true;
if (t.isJSXExpressionContainer(child)
&& !t.isJSXEmptyExpression(child.expression)
&& isLikelyTextExpression(child.expression)) return true;
}
return false;
}
/**
* Like {@link hasMeaningfulTextContent} but walks the full subtree.
* Used to distinguish decorative inline children (color swatches, icons)
* from ones that carry actual text.
*/
function subtreeHasText(elementNode) {
if (!t.isJSXElement(elementNode)) return false;
for (const child of elementNode.children ?? []) {
if (t.isJSXText(child) && child.value.trim().length > 0) return true;
if (t.isJSXExpressionContainer(child)
&& !t.isJSXEmptyExpression(child.expression)
&& isLikelyTextExpression(child.expression)) return true;
if (t.isJSXElement(child) && subtreeHasText(child)) return true;
}
return false;
}
/**
* Returns true if the element should receive an assisted-edit id due to spread
* props or dynamic children that contain text. Icon-only buttons are excluded.
* @param {import('@babel/types').JSXElement} elementNode
* @param {import('@babel/types').JSXOpeningElement} openingNode
* @returns {boolean}
*/
export function shouldDisableDueToChildren(elementNode, openingNode) {
if (!t.isJSXElement(elementNode) || !elementNode.children) return false;
const hasPropsSpread = openingNode.attributes.some(attribute =>
t.isJSXSpreadAttribute(attribute)
&& attribute.argument
&& t.isIdentifier(attribute.argument)
&& attribute.argument.name === 'props'
);
if (hasPropsSpread) return true;
const hasDynamicChild = elementNode.children.some(child => t.isJSXExpressionContainer(child));
if (hasDynamicChild) {
const hasStaticText = elementNode.children.some(
child => t.isJSXText(child) && child.value.trim().length > 0
);
if (hasStaticText) return true;
const hasTextLikeExpression = elementNode.children.some(child =>
t.isJSXExpressionContainer(child)
&& !t.isJSXEmptyExpression(child.expression)
&& isLikelyTextExpression(child.expression)
);
if (hasTextLikeExpression) return true;
}
return false;
}
/**
* Analyses child composition to decide whether the element can be edited.
* @param {import('@babel/types').JSXElement} elementNode
* @param {import('@babel/types').JSXOpeningElement} openingNode
* @returns {'skip'|'disabled'|'allow'}
*/
export function evalChildComposition(elementNode, openingNode) {
if (!t.isJSXElement(elementNode)) return 'allow';
if (!elementNode.children?.length) {
const name = openingNode?.name?.name || openingNode?.name?.property?.name || '';
return name === 'img' ? 'allow' : 'skip';
}
let hasTextContent = false;
let hasNonEditableJsxChild = false;
let hasNonSelfClosingChild = false;
let hasInlineTagChild = false;
let hasIdIneligibleInlineChild = false;
for (const child of elementNode.children) {
if (t.isJSXText(child)) {
if (child.value.trim().length > 0) hasTextContent = true;
continue;
}
if (t.isJSXExpressionContainer(child)) {
if (!t.isJSXEmptyExpression(child.expression) && isLikelyTextExpression(child.expression)) {
hasTextContent = true;
}
continue;
}
if (t.isJSXElement(child)) {
const childNode = child.openingElement;
if (childNode.selfClosing) {
const childName = childNode.name?.name || '';
if (!/^[A-Z]/.test(childName) && !checkTagNameEditable(childNode, EDITABLE_HTML_TAGS) && !INLINE_EDIT_HTML_TAGS.includes(childName)) {
hasNonEditableJsxChild = true;
}
continue;
}
const childTagName = child.openingElement.name?.name;
if (INLINE_EDIT_HTML_TAGS.includes(childTagName)) {
if (!subtreeHasText(child)) {
hasNonEditableJsxChild = true;
continue;
}
hasInlineTagChild = true;
// `strong`/`em`/… are inline-editable inside a parent but cannot
// receive their own edit id (Condition 2 returns early). Only
// skip the parent when every inline child could own an id.
if (!EDITABLE_HTML_TAGS.includes(childTagName)) {
hasIdIneligibleInlineChild = true;
}
continue;
}
hasNonSelfClosingChild = true;
if (!checkTagNameEditable(childNode, EDITABLE_HTML_TAGS)) {
hasNonEditableJsxChild = true;
}
}
}
if (!hasTextContent && !hasNonSelfClosingChild && !hasInlineTagChild) return 'skip';
if (hasNonEditableJsxChild) {
if (!hasTextContent && !hasInlineTagChild) return 'skip';
return 'disabled';
}
if (hasNonSelfClosingChild && !hasTextContent && !hasInlineTagChild) {
if (openingNode && checkTagNameEditable(openingNode, EDITABLE_HTML_TAGS)) return 'allow';
return 'skip';
}
// Layout wrappers that only nest id-eligible inline tags (span/a) skip so
// the child receives the id. Keep 'allow' when an inline child cannot own
// an id (`<p><strong>text</strong></p>`), or when the parent is itself an
// editable tag (h1, p, a, button, …) that should own the edit target.
if (!hasTextContent && hasInlineTagChild && !hasIdIneligibleInlineChild) {
const parentName = openingNode?.name?.name || openingNode?.name?.property?.name || '';
const isFormattingWrapper = INLINE_EDIT_HTML_TAGS.includes(parentName) && parentName !== 'a';
if (openingNode && checkTagNameEditable(openingNode, EDITABLE_HTML_TAGS)
&& !isFormattingWrapper) return 'allow';
return 'skip';
}
return 'allow';
}
/**
* Recursively checks whether an element or any of its descendants contain
* dynamic content, spread props, or conditional expressions that a direct
* inline edit would silently overwrite. When true the element must be routed
* through AI-assisted editing instead.
* @param {import('@babel/types').JSXElement} elementNode
* @returns {boolean}
*/
export function subtreeRequiresAssistance(elementNode) {
if (!t.isJSXElement(elementNode)) return false;
const opening = elementNode.openingElement;
if (opening.attributes.some(attribute => t.isJSXSpreadAttribute(attribute))) {
return true;
}
for (const child of elementNode.children ?? []) {
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
return true;
}
if (t.isJSXElement(child) && subtreeRequiresAssistance(child)) {
return true;
}
}
return false;
}
/**
* Returns true if an ancestor JSXElement already received a target attribute.
* @param {import('@babel/traverse').NodePath<import('@babel/types').JSXOpeningElement>} openingPath
* @returns {boolean}
*/
export function hasEditableAncestor(openingPath) {
const selfElementPath = openingPath.parentPath;
if (!selfElementPath?.isJSXElement()) return false;
let ancestorJsxElementPath = selfElementPath.findParent(path => path.isJSXElement());
while (ancestorJsxElementPath) {
const ancestorOpening = ancestorJsxElementPath.node.openingElement;
if (ancestorOpening.attributes.some(attribute =>
t.isJSXAttribute(attribute) && (attribute.name?.name === 'data-edit-id' || attribute.name?.name === 'data-edit-assisted-id')
)) {
return true;
}
ancestorJsxElementPath = ancestorJsxElementPath.findParent(path => path.isJSXElement());
}
return false;
}
/**
* Maps a JSX opening element tag name to a semantic edit type.
* @param {import('@babel/types').JSXOpeningElement} openingNode
* @returns {string|null}
*/
export function getElementTypeFromNode(openingNode) {
if (!openingNode || !openingNode.name) return null;
const name = openingNode.name?.name || openingNode.name?.property?.name || '';
if (!name) return null;
for (const [type, tags] of Object.entries(ELEMENT_TYPE_MAP)) {
if (tags.includes(name)) return type;
}
return null;
}