From 2123ae436156f144ee8f349185b2562f9883df7e Mon Sep 17 00:00:00 2001 From: babayaga Date: Fri, 26 Dec 2025 14:05:55 +0100 Subject: [PATCH] refactor polymech-astro --- packages/imagetools_3/astroViteConfigs.js | 8 +- packages/polymech/src/app/config-loader.ts | 3 +- .../src/components/modbus/CoilsTable.astro | 117 ++++++++ .../components/modbus/RegistersTable.astro | 258 ++++++++++++++++++ .../src/components/polymech/conditional.astro | 8 + .../src/components/polymech/image.astro | 22 ++ .../src/components/polymech/jsx.astro | 7 + .../src/components/polymech/link.astro | 11 + .../src/components/polymech/readme.astro | 48 ++++ .../src/components/polymech/remote.astro | 11 + .../src/components/polymech/renderer.ts | 53 ++++ .../src/components/polymech/resources.astro | 132 +++++++++ .../src/utils/markdown/rehype-custom-img.mjs | 36 +++ packages/polymech/src/utils/modbus-data.ts | 128 +++++++++ packages/polymech/src/utils/modbusUtils.ts | 128 +++++++++ 15 files changed, 964 insertions(+), 6 deletions(-) create mode 100644 packages/polymech/src/components/modbus/CoilsTable.astro create mode 100644 packages/polymech/src/components/modbus/RegistersTable.astro create mode 100644 packages/polymech/src/components/polymech/conditional.astro create mode 100644 packages/polymech/src/components/polymech/image.astro create mode 100644 packages/polymech/src/components/polymech/jsx.astro create mode 100644 packages/polymech/src/components/polymech/link.astro create mode 100644 packages/polymech/src/components/polymech/readme.astro create mode 100644 packages/polymech/src/components/polymech/remote.astro create mode 100644 packages/polymech/src/components/polymech/renderer.ts create mode 100644 packages/polymech/src/components/polymech/resources.astro create mode 100644 packages/polymech/src/utils/markdown/rehype-custom-img.mjs create mode 100644 packages/polymech/src/utils/modbus-data.ts create mode 100644 packages/polymech/src/utils/modbusUtils.ts diff --git a/packages/imagetools_3/astroViteConfigs.js b/packages/imagetools_3/astroViteConfigs.js index 2f7b5cc..8b2d327 100644 --- a/packages/imagetools_3/astroViteConfigs.js +++ b/packages/imagetools_3/astroViteConfigs.js @@ -1,12 +1,12 @@ export default { - "environment": "build", + "environment": "dev", "isSsrBuild": false, "projectBase": "", "publicDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\public\\", "rootDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\", - "mode": "production", - "outDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\dist\\", - "assetsDir": "_astro", + "mode": "dev", + "outDir": "dist", + "assetsDir": "/_astro", "sourcemap": false, "assetFileNames": "/_astro/[name]@[width].[hash][extname]" } \ No newline at end of file diff --git a/packages/polymech/src/app/config-loader.ts b/packages/polymech/src/app/config-loader.ts index 60c74f4..e31e9ef 100644 --- a/packages/polymech/src/app/config-loader.ts +++ b/packages/polymech/src/app/config-loader.ts @@ -22,8 +22,7 @@ export function loadConfig( } const variables = { - LANG: locale, - ...process.env + LANG: locale }; const substitutedContent = substitute(false, rawContent, variables); diff --git a/packages/polymech/src/components/modbus/CoilsTable.astro b/packages/polymech/src/components/modbus/CoilsTable.astro new file mode 100644 index 0000000..fe57101 --- /dev/null +++ b/packages/polymech/src/components/modbus/CoilsTable.astro @@ -0,0 +1,117 @@ +--- +import { getProcessedCoilsData } from '../../utils/modbus-data'; +import { getModbusFunctionName, getFunctionCategory } from '../../utils/modbusUtils'; + +const groupedCoils = getProcessedCoilsData(); +const groupNames = Object.keys(groupedCoils).sort(); +--- + +
+ {groupNames.map(groupName => ( +
+

{groupName}

+
+ + + + + + + + + + + + {groupedCoils[groupName].map(coil => { + return ( + + + + + + + + ); + })} + +
FCAddressNameComponentID
1/5{coil.address}{coil.name}{coil.component}{coil.id}
+
+
+ ))} +
+ + diff --git a/packages/polymech/src/components/modbus/RegistersTable.astro b/packages/polymech/src/components/modbus/RegistersTable.astro new file mode 100644 index 0000000..2df667d --- /dev/null +++ b/packages/polymech/src/components/modbus/RegistersTable.astro @@ -0,0 +1,258 @@ +--- +import { + getProcessedRegistersData, + getRegisterDescription, +} from "@polymech/astro-base/utils/modbus-data.js"; + +import { parseRegisterName } from "@polymech/astro-base/utils/modbusUtils.js"; + +const groupedRegisters = getProcessedRegistersData(); +const groupNames = Object.keys(groupedRegisters).sort(); +--- + +
+ { + groupNames.map((groupName) => ( +
+

{groupName}

+
+ + + + + + + + + + + + {groupedRegisters[groupName].map((register) => { + const parsed = parseRegisterName(register.name); + const baseName = register.name.split("(")[0]; + const description = getRegisterDescription(register.address); + + return ( + + + + + + + + ); + })} + +
FCAddressNameComponentDescription
{register.type}{register.address} +
+
{baseName}
+ {parsed && ( +
+ {parsed.enumValues.map(({ val, label }, index) => ( + + {val} + {label} + + ))} +
+ )} +
+
{register.component}{description || ""}
+
+
+ )) + } +
+ + diff --git a/packages/polymech/src/components/polymech/conditional.astro b/packages/polymech/src/components/polymech/conditional.astro new file mode 100644 index 0000000..eb2af1c --- /dev/null +++ b/packages/polymech/src/components/polymech/conditional.astro @@ -0,0 +1,8 @@ +--- +import DefaultFallback from "@/components/Default.astro" +const { expression = false, fallback = DefaultFallback } = Astro.props +const currentLocal = Astro.currentLocale || "en" +--- +
+ {expression ? :
} +
diff --git a/packages/polymech/src/components/polymech/image.astro b/packages/polymech/src/components/polymech/image.astro new file mode 100644 index 0000000..1649622 --- /dev/null +++ b/packages/polymech/src/components/polymech/image.astro @@ -0,0 +1,22 @@ +--- +import { DEFAULT_IMAGE_URL } from '@/app/config.js' +import { Picture } from "imagetools/components" +import { image_url } from "@/base/media.js" + +interface SafeImageProps { + src: string + alt: string + fallback?: string + [propName: string]: any +} + +const { + src, + alt, + fallback = DEFAULT_IMAGE_URL, + ...rest +} = Astro.props as SafeImageProps + +const srcSafe = await image_url(src, fallback) +--- + diff --git a/packages/polymech/src/components/polymech/jsx.astro b/packages/polymech/src/components/polymech/jsx.astro new file mode 100644 index 0000000..71a70b5 --- /dev/null +++ b/packages/polymech/src/components/polymech/jsx.astro @@ -0,0 +1,7 @@ +--- +import JsxParser from 'react-jsx-parser' +const {...rest} = Astro.props +--- + diff --git a/packages/polymech/src/components/polymech/link.astro b/packages/polymech/src/components/polymech/link.astro new file mode 100644 index 0000000..2517026 --- /dev/null +++ b/packages/polymech/src/components/polymech/link.astro @@ -0,0 +1,11 @@ +--- +const { href, className } = Astro.props; +--- +
  • + + + +
  • diff --git a/packages/polymech/src/components/polymech/readme.astro b/packages/polymech/src/components/polymech/readme.astro new file mode 100644 index 0000000..1bab92b --- /dev/null +++ b/packages/polymech/src/components/polymech/readme.astro @@ -0,0 +1,48 @@ +--- +import { createMarkdownComponent } from "@/base/index.js"; +import { translate } from "@/base/i18n.js"; +import { I18N_SOURCE_LANGUAGE, ASSET_URL } from "config/config.js"; +import { fromMarkdown } from "mdast-util-from-markdown"; +import { toMarkdown } from "mdast-util-to-markdown"; +import { visit } from "unist-util-visit"; +import { Root, Image } from "mdast"; + +interface Props { + markdown: string; + className?: string; + baseImageUrl?: string; + translate?: boolean; + data?: any; +} +const { + markdown, + className = "", + baseImageUrl = "", + translate: _translate = false, + data = {}, + ...rest +} = Astro.props as Props; + +const processImageUrls = (content: string, data: Record) => { + const tree = fromMarkdown(content) as Root; + visit(tree, "image", (node: Image) => { + if (!node.url.startsWith("http") && !node.url.startsWith("/")) { + node.url = ASSET_URL(node.url, data); + } + }); + const markup = toMarkdown(tree); + return markup; +}; + +const content = await translate( + processImageUrls(markdown, data), + I18N_SOURCE_LANGUAGE, + Astro.currentLocale, +); + +const ReadmeContent = await createMarkdownComponent(content); +--- + +
    + +
    diff --git a/packages/polymech/src/components/polymech/remote.astro b/packages/polymech/src/components/polymech/remote.astro new file mode 100644 index 0000000..8c66509 --- /dev/null +++ b/packages/polymech/src/components/polymech/remote.astro @@ -0,0 +1,11 @@ +--- +// Example: Fetch Markdown from a remote API +// and render it to HTML, at runtime. +// Using "marked" (https://github.com/markedjs/marked) +import { marked } from 'marked' +const {...rest} = Astro.props +const response = await fetch(rest.src || 'https://raw.githubusercontent.com/wiki/adam-p/markdown-here/Markdown-Cheatsheet.md') +const markdown = await response.text(); +const content = marked.parse(markdown); +--- +
    \ No newline at end of file diff --git a/packages/polymech/src/components/polymech/renderer.ts b/packages/polymech/src/components/polymech/renderer.ts new file mode 100644 index 0000000..172ae99 --- /dev/null +++ b/packages/polymech/src/components/polymech/renderer.ts @@ -0,0 +1,53 @@ +import { unified } from "unified" +import remarkParse from "remark-parse" +import remarkGfm from "remark-gfm" +import remarkRehype from "remark-rehype" +import rehypeRaw from "rehype-raw" +import emoji from "remark-emoji" +import rehypeStringify from "rehype-stringify" +import { createComponent } from "astro/runtime/server/astro-component.js" +import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js" + +// Define the type for the component map +type ComponentMap = Record) => unknown>; + +// Function to convert Markdown to HTML and replace elements with Astro components +export async function markdown(input: string, componentsMap: ComponentMap = {}): Promise { + const slots: unknown[] = []; + let slotIndex = 0; + + // Ensure input is treated as UTF-8 + const markdownText = new TextDecoder("utf-8").decode(new TextEncoder().encode(input)); + + const processedHtml = await unified() + .use(remarkParse) // Parse Markdown to AST + .use(emoji, { + accessible: true, // Defaults to false + emoticon: true, // Defaults to false + }) + .use(remarkGfm) // Enable tables, strikethrough, autolinks, etc. + .use(remarkRehype, { allowDangerousHtml: true }) // Convert Markdown to HTML AST + .use(rehypeRaw) // Process raw HTML inside Markdown + .use(() => (tree) => { + function transformNode(node: { type: string; tagName?: string; properties?: Record; value?: string; children?: any[] }) { + if (node.type === "element" && node.tagName && componentsMap[node.tagName]) { + const props = node.properties || {}; + + // Register the component as a slot instead of calling it directly + const slotName = `COMPONENT_SLOT_${slotIndex++}`; + slots[slotName] = { component: componentsMap[node.tagName], props }; + node.type = "text"; + node.value = ``; // Slot placeholder + } + + if (node.children) { + node.children.forEach(transformNode); + } + } + transformNode(tree); + }) + .use(rehypeStringify) // Convert AST back to HTML + .process(markdownText); + + return createComponent(() => renderTemplate(unescapeHTML(processedHtml.toString()), slots)); +} diff --git a/packages/polymech/src/components/polymech/resources.astro b/packages/polymech/src/components/polymech/resources.astro new file mode 100644 index 0000000..b2b6344 --- /dev/null +++ b/packages/polymech/src/components/polymech/resources.astro @@ -0,0 +1,132 @@ +--- +import Link from "./link.astro"; +import { createMarkdownComponent } from "@/base/index.js"; +import Translation from "@polymech/astro-base/components/i18n.astro"; + +const { frontmatter: data } = Astro.props; +const LINK_CLASSES = "text-blue-400 hover:text-blue-800 hover:underline"; + +const getHref = (key, data) => { + switch (key) { + case "cad": + return data.cad?.[0]?.[".html"]; + default: + return data[key]; + } +}; + +const checkCondition = (key, data) => { + switch (key) { + case "cad": + return !!data.Preview3d && !!data.cad?.[0]?.[".html"]; + default: + return true; + } +}; + +const links = (data) => + ["forum", "firmware", "download", "tests", "cad"].map((key) => { + return { + key, + label: key.charAt(0).toUpperCase() + key.slice(1), + getHref: () => getHref(key, data), + condition: () => checkCondition(key, data), + }; + }); + +const componentsLinks = (data) => + data.components?.map((comp) => ({ + key: comp.name, + label: comp.name, + getHref: () => comp.store, + condition: () => !!comp.store, + })) || []; + +const authorsLinks = (data) => + data.authors?.map((author) => ({ + key: author.name, + label: author.name, + getHref: () => author.url, + condition: () => !!author.url, + })) || []; + +const groups = { + Resources: links(data), + ...(data.components && { Components: componentsLinks(data) }), + ...(data.authors && { Authors: authorsLinks(data) }), +}; + +const filteredGroups: Record< + string, + { + key: string; + label: string; + getHref: () => string; + condition: () => boolean; + }[] +> = Object.entries(groups).reduce((acc, [name, links]) => { + const validLinks = (links as any[]).filter( + (link) => link.getHref() && link.condition(), + ); + if (validLinks.length > 0) { + acc[name] = validLinks; + } + return acc; +}, {}); + +const extraContent = [data.extra_resources, data.tests] + .filter((s) => s && s.length) + .map(createMarkdownComponent) + +const sharedContent = [data.shared_resources] + .filter((s) => s && s.length) + .map(createMarkdownComponent) +--- + +
    +
    +
    + { + Object.entries(filteredGroups).map(([name, links]) => ( +
    +

    + {name} +

    +
      + {links.map((link) => ( + + {link.label} + + ))} +
    +
    + )) + } +
    + { + extraContent.length > 0 && ( +
    +

    + Extras +

    +
    + {extraContent.map(async (Content) => ( + + ))} +
    +
    + ) + } + { + sharedContent.length > 0 && ( +
    +
    + {sharedContent.map(async (Content) => ( + + ))} +
    +
    + ) + } +
    +
    diff --git a/packages/polymech/src/utils/markdown/rehype-custom-img.mjs b/packages/polymech/src/utils/markdown/rehype-custom-img.mjs new file mode 100644 index 0000000..9dd07bb --- /dev/null +++ b/packages/polymech/src/utils/markdown/rehype-custom-img.mjs @@ -0,0 +1,36 @@ +import { visit } from 'unist-util-visit'; +import path from 'path'; + +export default function rehypeCustomImg() { + return (tree, file) => { + if(!file.path.endsWith('.mdx')) { + return; + } + + const contentDir = path.join(process.cwd(), 'src', 'content'); + const entryPath = path.relative(contentDir, file.history[0]).replace(/\\/g, '/'); + + // 1. Add the import statement for RelativeImage. + tree.children.unshift({ + type: 'mdxjsEsm', + value: "import RelativeImage from '~/components/imagetools/RelativeImage.astro';" + }); + + // 2. Visit all JSX nodes and inject entryPath into tags. + visit(tree, 'mdxJsxFlowElement', (node) => { + if (node.name === 'img') { + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'entryPath', + value: entryPath + }); + } + }); + + // 3. Export the components mapping. + tree.children.push({ + type: 'mdxjsEsm', + value: 'export const components = { img: RelativeImage };' + }); + }; +} diff --git a/packages/polymech/src/utils/modbus-data.ts b/packages/polymech/src/utils/modbus-data.ts new file mode 100644 index 0000000..7a75741 --- /dev/null +++ b/packages/polymech/src/utils/modbus-data.ts @@ -0,0 +1,128 @@ +import fs from 'fs'; +import path from 'path'; + +export interface CoilData { + address: number; + value: number; + name: string; + component: string; + id: number; + group: string; +} + +export interface RegisterData { + error: number; + address: number; + value: number; + name: string; + component: string; + id: number; + type: number; + slaveId: number; + flags: number; + group: string; +} + +export interface GroupedData { + [groupName: string]: T[]; +} + +export interface RegDescription { + address: number; + description: string; +} + +/** + * Read and parse coils.json file + */ +export function readCoilsData(): CoilData[] { + try { + const filePath = path.join(process.cwd(), 'src/content/resources/cassandra/coils.json'); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(fileContent) as CoilData[]; + } catch (error) { + console.error('Error reading coils.json:', error); + return []; + } +} + +/** + * Read and parse registers.json file + */ +export function readRegistersData(): RegisterData[] { + try { + const filePath = path.join(process.cwd(), 'src/content/resources/cassandra/registers.json'); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(fileContent) as RegisterData[]; + } catch (error) { + console.error('Error reading registers.json:', error); + return []; + } +} + +/** + * Group data by the 'group' field + */ +export function groupData(data: T[]): GroupedData { + return data.reduce((acc, item) => { + if (!acc[item.group]) { + acc[item.group] = []; + } + acc[item.group].push(item); + return acc; + }, {} as GroupedData); +} + +/** + * Sort grouped data by address within each group + */ +export function sortGroupedDataByAddress(groupedData: GroupedData): GroupedData { + const sorted: GroupedData = {}; + + Object.keys(groupedData).forEach(group => { + sorted[group] = groupedData[group].sort((a, b) => a.address - b.address); + }); + + return sorted; +} + +/** + * Get processed coils data grouped by component/group + */ +export function getProcessedCoilsData(): GroupedData { + const coilsData = readCoilsData(); + const groupedData = groupData(coilsData); + return sortGroupedDataByAddress(groupedData); +} + +/** + * Read and parse regs.json file for descriptions + */ +export function readRegDescriptions(): RegDescription[] { + try { + const filePath = path.join(process.cwd(), 'src/content/resources/cassandra/regs.json'); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(fileContent) as RegDescription[]; + } catch (error) { + console.error('Error reading regs.json:', error); + return []; + } +} + +/** + * Get processed registers data grouped by component/group + */ +export function getProcessedRegistersData(): GroupedData { + const registersData = readRegistersData(); + const groupedData = groupData(registersData); + return sortGroupedDataByAddress(groupedData); +} + +/** + * Get description for a specific register address + */ +export function getRegisterDescription(address: number): string | null { + const descriptions = readRegDescriptions(); + const found = descriptions.find(desc => desc.address === address); + return found ? found.description : null; +} diff --git a/packages/polymech/src/utils/modbusUtils.ts b/packages/polymech/src/utils/modbusUtils.ts new file mode 100644 index 0000000..0da19b5 --- /dev/null +++ b/packages/polymech/src/utils/modbusUtils.ts @@ -0,0 +1,128 @@ +export interface EnumValue { + val: number; + label: string; +} + +export interface ParsedRegister { + enumValues: EnumValue[]; + isFlags?: boolean; +} + +/** + * Parse register name to extract enum values and flag information + * Examples: + * - "Status(0:IDLE, 1:INITIALIZING, 2:RUNNING, 3:PAUSED, 4:STOPPED, 5:FINISHED)" + * - "CFlags(1:MINLOAD,2:MAX_TIME,4:STALLED,8:BALANCE,16:LOADCELL,32:MULTI_TIMEOUT)" + * - "Mode(0:NONE,1:MANUAL,2:AUTO,3:MANUAL_MULTI,4:AUTO_MULTI,5:AUTO_MULTI_BALANCED,6:REMOTE)" + */ +export function parseRegisterName(name: string): ParsedRegister | null { + if (!name) return null; + + // Look for pattern like "Name(0:VALUE, 1:VALUE2, ...)" + const enumMatch = name.match(/\(([^)]+)\)/); + if (!enumMatch) return null; + + const enumString = enumMatch[1]; + const enumValues: EnumValue[] = []; + + // Split by comma and parse each enum value + const parts = enumString.split(','); + + for (const part of parts) { + const trimmed = part.trim(); + // Match pattern like "0:IDLE" or "1:INITIALIZING" + const valueMatch = trimmed.match(/^(\d+):(.+)$/); + + if (valueMatch) { + const val = parseInt(valueMatch[1], 10); + const label = valueMatch[2].trim(); + + if (!isNaN(val)) { + enumValues.push({ val, label }); + } + } + } + + if (enumValues.length === 0) return null; + + // Determine if this is flags based on the name or values + const isFlags = name.toLowerCase().includes('flags') || + name.toLowerCase().includes('cflags') || + // Check if values are powers of 2 (typical for flags) + enumValues.every(ev => ev.val > 0 && (ev.val & (ev.val - 1)) === 0); + + return { + enumValues, + isFlags + }; +} + +/** + * Format enum values for display + */ +export function formatEnumValues(enumValues: EnumValue[]): string { + return enumValues.map(ev => `${ev.val}:${ev.label}`).join(', '); +} + +/** + * Get the label for a specific enum value + */ +export function getEnumLabel(enumValues: EnumValue[], value: number): string | null { + const found = enumValues.find(ev => ev.val === value); + return found ? found.label : null; +} + +/** + * Get active flag labels for a flag value + */ +export function getActiveFlagLabels(enumValues: EnumValue[], value: number): string[] { + return enumValues + .filter(ev => (value & ev.val) === ev.val && ev.val > 0) + .map(ev => ev.label); +} + +/** + * Modbus function type mappings + */ +export const MODBUS_FUNCTION_TYPES = { + // Coil functions + 1: 'Read Coils', + 5: 'Write Single Coil', + 15: 'Write Multiple Coils', + + // Discrete Input functions + 2: 'Read Discrete Inputs', + + // Holding Register functions + 3: 'Read Holding Registers', + 6: 'Write Single Register', + 16: 'Write Multiple Registers', + + // Input Register functions + 4: 'Read Input Registers' +} as const; + +/** + * Get the Modbus function name from type number + */ +export function getModbusFunctionName(type: number): string { + return MODBUS_FUNCTION_TYPES[type as keyof typeof MODBUS_FUNCTION_TYPES] || `Function ${type}`; +} + +/** + * Determine if a register/coil is writable based on its type + */ +export function isWritableFunction(type: number): boolean { + return [5, 6, 15, 16].includes(type); +} + +/** + * Get function type category + */ +export function getFunctionCategory(type: number): 'coil' | 'discrete' | 'holding' | 'input' | 'unknown' { + if ([1, 5, 15].includes(type)) return 'coil'; + if ([2].includes(type)) return 'discrete'; + if ([3, 6, 16].includes(type)) return 'holding'; + if ([4].includes(type)) return 'input'; + return 'unknown'; +}