refactor polymech-astro
This commit is contained in:
parent
5df7b5915f
commit
2123ae4361
@ -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]"
|
||||
}
|
||||
@ -22,8 +22,7 @@ export function loadConfig(
|
||||
}
|
||||
|
||||
const variables = {
|
||||
LANG: locale,
|
||||
...process.env
|
||||
LANG: locale
|
||||
};
|
||||
|
||||
const substitutedContent = substitute(false, rawContent, variables);
|
||||
|
||||
117
packages/polymech/src/components/modbus/CoilsTable.astro
Normal file
117
packages/polymech/src/components/modbus/CoilsTable.astro
Normal file
@ -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();
|
||||
---
|
||||
|
||||
<div class="modbus-coils-tables">
|
||||
{groupNames.map(groupName => (
|
||||
<div class="group-section">
|
||||
<h3>{groupName}</h3>
|
||||
<div class="table-container">
|
||||
<table class="modbus-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FC</th>
|
||||
<th>Address</th>
|
||||
<th>Name</th>
|
||||
<th>Component</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedCoils[groupName].map(coil => {
|
||||
return (
|
||||
<tr>
|
||||
<td class="fc-cell">1/5</td>
|
||||
<td>{coil.address}</td>
|
||||
<td>{coil.name}</td>
|
||||
<td>{coil.component}</td>
|
||||
<td>{coil.id}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modbus-coils-tables {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.group-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.group-section h3 {
|
||||
color: var(--theme-text-accent);
|
||||
border-bottom: 2px solid var(--theme-accent);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--theme-bg-accent);
|
||||
}
|
||||
|
||||
.modbus-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--theme-bg);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.modbus-table th {
|
||||
background: var(--theme-bg-accent);
|
||||
color: var(--theme-text-accent);
|
||||
font-weight: 600;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid var(--theme-accent);
|
||||
}
|
||||
|
||||
.modbus-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--theme-bg-accent);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.modbus-table tbody tr:hover {
|
||||
background: var(--theme-bg-accent);
|
||||
}
|
||||
|
||||
.modbus-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* FC cell styling */
|
||||
.fc-cell {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--theme-text-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.modbus-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.modbus-table th,
|
||||
.modbus-table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
258
packages/polymech/src/components/modbus/RegistersTable.astro
Normal file
258
packages/polymech/src/components/modbus/RegistersTable.astro
Normal file
@ -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();
|
||||
---
|
||||
|
||||
<div class="modbus-registers-tables">
|
||||
{
|
||||
groupNames.map((groupName) => (
|
||||
<div class="group-section">
|
||||
<h3>{groupName}</h3>
|
||||
<div class="table-container">
|
||||
<table class="modbus-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FC</th>
|
||||
<th>Address</th>
|
||||
<th>Name</th>
|
||||
<th>Component</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedRegisters[groupName].map((register) => {
|
||||
const parsed = parseRegisterName(register.name);
|
||||
const baseName = register.name.split("(")[0];
|
||||
const description = getRegisterDescription(register.address);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td class="fc-cell">{register.type}</td>
|
||||
<td>{register.address}</td>
|
||||
<td>
|
||||
<div class="name-cell">
|
||||
<div class="base-name">{baseName}</div>
|
||||
{parsed && (
|
||||
<div
|
||||
class={`enum-values ${parsed.isFlags ? "flags" : "enum"}`}
|
||||
>
|
||||
{parsed.enumValues.map(({ val, label }, index) => (
|
||||
<span
|
||||
class="enum-item"
|
||||
data-key={`${register.address}-${val}-${index}`}
|
||||
>
|
||||
<span class="enum-value">{val}</span>
|
||||
<span class="enum-label">{label}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{register.component}</td>
|
||||
<td class="description-cell">{description || ""}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modbus-registers-tables {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.group-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.group-section h3 {
|
||||
color: var(--theme-text-accent);
|
||||
border-bottom: 2px solid var(--theme-accent);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--theme-bg-accent);
|
||||
}
|
||||
|
||||
.modbus-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--theme-bg);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.modbus-table th {
|
||||
background: var(--theme-bg-accent);
|
||||
color: var(--theme-text-accent);
|
||||
font-weight: 600;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid var(--theme-accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modbus-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--theme-bg-accent);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.modbus-table tbody tr:hover {
|
||||
background: var(--theme-bg-accent);
|
||||
}
|
||||
|
||||
.modbus-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Name cell styling */
|
||||
.name-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.base-name {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
/* Enum and flag styling */
|
||||
.enum-values {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.enum-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
background: var(--theme-bg-accent);
|
||||
border: 1px solid var(--theme-accent-light);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.enum-values.enum .enum-item {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.enum-values.flags .enum-item {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.enum-value {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-accent);
|
||||
}
|
||||
|
||||
.enum-label {
|
||||
font-family: monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--theme-text-light);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* FC cell styling */
|
||||
.fc-cell {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--theme-text-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Description cell styling */
|
||||
.description-cell {
|
||||
font-style: italic;
|
||||
color: var(--theme-text-light);
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.modbus-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.modbus-table th,
|
||||
.modbus-table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
min-width: 150px;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.enum-values {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.enum-item {
|
||||
padding: 0.1rem 0.25rem;
|
||||
}
|
||||
|
||||
.enum-label {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.table-container {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modbus-table th,
|
||||
.modbus-table td {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.enum-values {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.enum-item {
|
||||
padding: 0.08rem 0.2rem;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.enum-label {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,8 @@
|
||||
---
|
||||
import DefaultFallback from "@/components/Default.astro"
|
||||
const { expression = false, fallback = DefaultFallback } = Astro.props
|
||||
const currentLocal = Astro.currentLocale || "en"
|
||||
---
|
||||
<div data-track={currentLocal}>
|
||||
{expression ? <slot /> : <div/>}
|
||||
</div>
|
||||
22
packages/polymech/src/components/polymech/image.astro
Normal file
22
packages/polymech/src/components/polymech/image.astro
Normal file
@ -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)
|
||||
---
|
||||
<Picture class="" src={srcSafe} alt={alt} {...rest} />
|
||||
7
packages/polymech/src/components/polymech/jsx.astro
Normal file
7
packages/polymech/src/components/polymech/jsx.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
import JsxParser from 'react-jsx-parser'
|
||||
const {...rest} = Astro.props
|
||||
---
|
||||
<JsxParser
|
||||
jsx={rest.markup}
|
||||
/>
|
||||
11
packages/polymech/src/components/polymech/link.astro
Normal file
11
packages/polymech/src/components/polymech/link.astro
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
const { href, className } = Astro.props;
|
||||
---
|
||||
<li>
|
||||
<a
|
||||
href={href}
|
||||
class={className}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</li>
|
||||
48
packages/polymech/src/components/polymech/readme.astro
Normal file
48
packages/polymech/src/components/polymech/readme.astro
Normal file
@ -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<string, string>) => {
|
||||
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);
|
||||
---
|
||||
|
||||
<div class={className}>
|
||||
<ReadmeContent />
|
||||
</div>
|
||||
11
packages/polymech/src/components/polymech/remote.astro
Normal file
11
packages/polymech/src/components/polymech/remote.astro
Normal file
@ -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);
|
||||
---
|
||||
<article set:html={content} />
|
||||
53
packages/polymech/src/components/polymech/renderer.ts
Normal file
53
packages/polymech/src/components/polymech/renderer.ts
Normal file
@ -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<string, (props: Record<string, string>) => unknown>;
|
||||
|
||||
// Function to convert Markdown to HTML and replace elements with Astro components
|
||||
export async function markdown(input: string, componentsMap: ComponentMap = {}): Promise<unknown> {
|
||||
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<string, string>; 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 = `<!--${slotName}-->`; // 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));
|
||||
}
|
||||
132
packages/polymech/src/components/polymech/resources.astro
Normal file
132
packages/polymech/src/components/polymech/resources.astro
Normal file
@ -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)
|
||||
---
|
||||
|
||||
<section class="p-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{
|
||||
Object.entries(filteredGroups).map(([name, links]) => (
|
||||
<div class="p-2">
|
||||
<h3 class="text-xl font-bold mb-2">
|
||||
<Translation>{name}</Translation>
|
||||
</h3>
|
||||
<ul class="list-disc pl-6 space-y-1">
|
||||
{links.map((link) => (
|
||||
<Link href={link.getHref()} className={LINK_CLASSES}>
|
||||
<Translation>{link.label}</Translation>
|
||||
</Link>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
extraContent.length > 0 && (
|
||||
<div class="p-2">
|
||||
<h3 class="text-xl font-bold mb-2">
|
||||
<Translation>Extras</Translation>
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 extra-resources">
|
||||
{extraContent.map(async (Content) => (
|
||||
<Content class="extra-resource" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
sharedContent.length > 0 && (
|
||||
<div class="p-2">
|
||||
<div class="grid grid-cols-1 gap-4 extra-resources">
|
||||
{sharedContent.map(async (Content) => (
|
||||
<Content class="extra-resource" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
36
packages/polymech/src/utils/markdown/rehype-custom-img.mjs
Normal file
36
packages/polymech/src/utils/markdown/rehype-custom-img.mjs
Normal file
@ -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 <img> 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 };'
|
||||
});
|
||||
};
|
||||
}
|
||||
128
packages/polymech/src/utils/modbus-data.ts
Normal file
128
packages/polymech/src/utils/modbus-data.ts
Normal file
@ -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<T> {
|
||||
[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<T extends { group: string }>(data: T[]): GroupedData<T> {
|
||||
return data.reduce((acc, item) => {
|
||||
if (!acc[item.group]) {
|
||||
acc[item.group] = [];
|
||||
}
|
||||
acc[item.group].push(item);
|
||||
return acc;
|
||||
}, {} as GroupedData<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort grouped data by address within each group
|
||||
*/
|
||||
export function sortGroupedDataByAddress<T extends { address: number }>(groupedData: GroupedData<T>): GroupedData<T> {
|
||||
const sorted: GroupedData<T> = {};
|
||||
|
||||
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<CoilData> {
|
||||
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<RegisterData> {
|
||||
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;
|
||||
}
|
||||
128
packages/polymech/src/utils/modbusUtils.ts
Normal file
128
packages/polymech/src/utils/modbusUtils.ts
Normal file
@ -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';
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user