diff --git a/packages/polymech/components/i18n.astro b/packages/polymech/components/i18n.astro
deleted file mode 100644
index 80eebfc..0000000
--- a/packages/polymech/components/i18n.astro
+++ /dev/null
@@ -1,22 +0,0 @@
----
-import { I18N_SOURCE_LANGUAGE } from "config/config.js"
-import { translate, IOptions } from '@/base/i18n.js'
-
-export interface Props extends IOptions {
- language?: string,
- clazz?:string
-}
-
-const {
- language = Astro.currentLocale,
- clazz = '',
- ...rest
-} = Astro.props
-
-const content = await Astro.slots.render('default')
-const translatedText = await translate(content, I18N_SOURCE_LANGUAGE, language, rest)
-
----
-
- {translatedText}
-
diff --git a/packages/polymech/package.json b/packages/polymech/package.json
index befb811..42ad866 100644
--- a/packages/polymech/package.json
+++ b/packages/polymech/package.json
@@ -6,12 +6,16 @@
"dev": "tsc -p . --watch"
},
"exports": {
- ".": "./index.ts"
+ ".": "./dist/index.js",
+ "./components/*": "./src/components/*"
},
"files": [
- "index.ts"
+ "dist/",
+ "src/components/"
],
"dependencies": {
+ "@astrojs/compiler": "^2.12.2",
+ "@astrojs/react": "^4.3.0",
"@polymech/cad": "file:../../../polymech-mono/packages/cad",
"@polymech/commons": "file:../../../polymech-mono/packages/commons",
"@polymech/fs": "file:../../../polymech-mono/packages/fs",
@@ -22,6 +26,7 @@
"exifreader": "^4.31.1",
"find-up": "^7.0.0",
"github-slugger": "^2.0.0",
+ "glob": "^11.0.3",
"html-entities": "^2.5.2",
"imagetools": "file:../imagetools",
"marked": "^16.1.2",
@@ -32,6 +37,8 @@
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"showdown": "^2.1.0",
- "unified": "^11.0.5"
+ "tslog": "^4.9.3",
+ "unified": "^11.0.5",
+ "yargs": "^18.0.0"
}
}
diff --git a/packages/polymech/src/app/cli.ts b/packages/polymech/src/app/cli.ts
new file mode 100644
index 0000000..5dafcd2
--- /dev/null
+++ b/packages/polymech/src/app/cli.ts
@@ -0,0 +1,9 @@
+import cli from 'yargs'
+import { hideBin } from 'yargs/helpers'
+import { } from './network.js'
+
+const argv = cli(hideBin(process.argv)).parse()
+
+export const options = () => {
+ console.log('Options: ', argv)
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/config.json b/packages/polymech/src/app/config.json
new file mode 100644
index 0000000..8d982c6
--- /dev/null
+++ b/packages/polymech/src/app/config.json
@@ -0,0 +1,112 @@
+{
+ "site": {
+ "title": "Polymech",
+ "base_url": "https://polymech.io/",
+ "description" : "",
+ "base_path": "/",
+ "trailing_slash": false,
+ "favicon": "/images/favicon.png",
+ "logo": "/images/logo.png",
+ "logo_darkmode": "/images/logo-darkmode.png",
+ "logo_width": "150",
+ "logo_height": "33",
+ "logo_text": "Astrofront",
+ "image": {
+ "default": "/images/default-image.png",
+ "error": "/images/error-image.png",
+ "alt": "Astrofront"
+ }
+ },
+ "footer_left": [
+ {
+ "href": "/rss.xml",
+ "text": "RSS"
+ },
+ {
+ "href": "/helpcenter/home",
+ "text": "Home"
+ },
+ {
+ "href": "/infopages/dpa",
+ "text": "DPA"
+ },
+ {
+ "href": "/infopages/cookies",
+ "text": "Cookies"
+ },
+ {
+ "href": "/infopages/terms",
+ "text": "Terms"
+ },
+ {
+ "href": "/infopages/privacy",
+ "text": "Privacy"
+ },
+ {
+ "href": "/infopages/cookies",
+ "text": "Cookies"
+ },
+ {
+ "href": "/forms/contact",
+ "text": "Contact"
+ },
+ {
+ "href": "/infopages/about",
+ "text": "About us"
+ },
+ {
+ "href": "/404",
+ "text": "Error 404"
+ }
+ ],
+ "footer_right": [
+
+ ],
+ "settings": {
+ "search": true,
+ "account": true,
+ "sticky_header": true,
+ "theme_switcher": true,
+ "default_theme": "system"
+ },
+ "params": {
+ "contact_form_action": "#",
+ "copyright": "Designed And Developed by [Themefisher](https://themefisher.com/)"
+ },
+ "navigation_button": {
+ "enable": true,
+ "label": "Get Started",
+ "link": "https://github.com/themefisher/astrofront"
+ },
+ "ecommerce": {
+ "brand": "Polymech",
+ "currencySymbol": "",
+ "currencyCode": "EU"
+ },
+ "metadata": {
+ "country": "Spain",
+ "city": "Barcelona",
+ "author": "Polymech",
+ "author_bio": "I am in, if its true",
+ "author_url": "https://polymech.io/",
+ "image": "/images/og-image.png",
+ "description": "Polymech is a plastic prototyping company that offers product design services.",
+ "keywords": "Plastic, Prototyping, Product Design, Opensource"
+ },
+ "shopify": {
+ "currencySymbol": "",
+ "currencyCode": "EU",
+ "collections": {
+ "hero_slider": "hidden-homepage-carousel",
+ "featured_products": "featured-products"
+ }
+ },
+ "pages":{
+ "home":{
+ "hero": "https://assets.osr-plastic.org/machines//assets/newsletter/common/products/extruders/overview-3.jpg",
+ "_blog":{
+ "store": "posts"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/config.ts b/packages/polymech/src/app/config.ts
new file mode 100644
index 0000000..f7b38f3
--- /dev/null
+++ b/packages/polymech/src/app/config.ts
@@ -0,0 +1,168 @@
+import * as path from 'path'
+import { IMAGE_PRESET, E_BROADBAND_SPEED } from "./network.js"
+import { resolve, template } from '@polymech/commons'
+import { sync as read } from '@polymech/fs/read'
+import { sanitizeUri } from 'micromark-util-sanitize-uri'
+
+export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}"))
+
+export const LOGGING_NAMESPACE = 'polymech-site'
+export const TRANSLATE_CONTENT = true
+export const LANGUAGES = ['en', 'ar', 'de', 'ja', 'es', 'zh']
+//export const LANGUAGES_PROD = ['en']
+export const LANGUAGES_PROD = ['en', 'es', 'ar', 'de', 'ja', 'zh', 'fr']
+export const isRTL = (lang) => lang === 'ar'
+
+// i18n constants
+export const I18N_STORE = (root, lang) => `${root}/i18n-store/store-${lang}.json`
+export const I18N_SOURCE_LANGUAGE = 'en'
+export const I18N_CACHE = true
+export const I18N_ASSET_PATH = "${SRC_DIR}/${SRC_NAME}-${DST_LANG}${SRC_EXT}"
+
+// Products
+export const PRODUCT_ROOT = () => path.resolve(resolve("${OSR_ROOT}/products"))
+export const PRODUCT_BRANCHES = read(path.join(PRODUCT_ROOT(), 'config/machines.json'), 'json')
+export const PRODUCT_GLOB = '**/config.json'
+
+// Product compiler
+export const PRODUCT_CONFIG = (product) =>
+ path.resolve(resolve(`${PRODUCT_ROOT()}/${product}/config.json`, false,
+ {
+ product
+ }))
+export const PRODUCT_DIR = (product) => path.resolve(resolve(`${PRODUCT_ROOT()}/${product}`))
+export const PRODUCT_HUGO_TEMPLATE = './osr/hugo/root.html'
+export const PRODUCTS_TARGET_SRC = './src/content/en/retail'
+export const PRODUCTS_TARGET = (lang) => `./content/${lang}/products`
+
+// OSRL - Language
+export const IS_DEV = true
+export const OSRL_ENV = 'astro-release'
+export const OSRL_ENV_DEV = 'astro-debug'
+export const OSRL_ENVIRONMENT = IS_DEV ? OSRL_ENV_DEV : OSRL_ENV
+export const OSRL_MODULE_NAME = 'polymech.io'
+export const OSRL_PRODUCT_PROFILE = './src/app/profile.json'
+export const OSRL_LANG_FLAVOR = 'osr'
+
+// Products
+export const ENABLED_PRODUCTS = "${OSR_ROOT}/products/config/machines.json"
+export const PRODUCT_SPECS = (rel) => `${PRODUCT_ROOT()}/${rel}/specs.xlsx`
+
+// Tasks
+export const TASK_CONFIG_LOG_DIRECTORY = './config/'
+
+// Task: compile:content
+export const TASK_COMPILE_CONTENT = true
+export const TASK_COMPILE_CONTENT_CACHE = false
+
+// Task - Logging
+export const TASK_LOG_DIRECTORY = './logs/'
+
+// Task - Retail Config
+export const REGISTER_PRODUCT_TASKS = true
+export const RETAIL_PRODUCT_BRANCH = 'site'
+export const RETAIL_COMPILE_CACHE = false
+export const RETAIL_MEDIA_CACHE = true
+export const RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS = 'info'
+
+export const ConvertProductMedia = true
+export const TranslateProductAssets = false
+export const PopulateProductDefaults = true
+
+// CAD
+export const CAD_MAIN_MATCH = (product) => `${product}/cad*/*Global*.+(SLDASM)`
+export const CAD_CAM_MAIN_MATCH = (product) => `${product}/cad*/*-CNC*.+(SLDASM)`
+
+export const CAD_CACHE = true
+export const CAD_EXPORT_CONFIGURATIONS = true
+export const CAD_EXPORT_SUB_COMPONENTS = true
+export const CAD_MODEL_FILE_PATH = (SOURCE, CONFIGURATION = '') =>
+ SOURCE.replace('.json', `${CONFIGURATION ? '-' + CONFIGURATION : ''}.tree.json`)
+export const CAD_DEFAULT_CONFIGURATION = 'Default'
+export const CAD_RENDERER = 'solidworks'
+export const CAD_RENDERER_VIEW = 'Render'
+export const CAD_RENDERER_QUALITY = 1
+export const CAD_EXTENSIONS = ['.STEP', '.html']
+export const CAD_MODEL_EXT = '.tree.json'
+
+export const CAD_URL = (file: string, variables: Record) =>
+ sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/${file}", { file, ...variables }))
+
+export const ASSET_URL = (file: string, variables: Record) =>
+ sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/products/${product_rel_min}/${file}", { file, ...variables }))
+
+export const ITEM_ASSET_URL = (variables: Record) =>
+ template("${OSR_MACHINES_ASSETS_URL}/${ITEM_REL}/${assetPath}/${filePath}", variables)
+
+
+//back compat - osr-cad
+export const parseBoolean = (value: string): boolean => {
+ return value === '1' || value.toLowerCase() === 'true';
+}
+/////////////////////////////////////////////
+//
+// Rendering
+export const SHOW_DESCRIPTION = false
+export const SHOW_LICENSE = false
+export const SHOW_RENDERINGS = true
+
+export const SHOW_TABS = false
+export const SHOW_GALLERY = false
+export const SHOW_FILES = true
+export const SHOW_SPECS = true
+export const SHOW_CHECKOUT = false
+export const SHOW_3D_PREVIEW = true
+export const SHOW_RESOURCES = true
+export const SHOW_DEBUG = false
+export const SHOW_SAMPLES = true
+export const SHOW_README = false
+
+/////////////////////////////////////////////
+//
+// Plugins
+
+// RSS
+export const RSS_CONFIG =
+{
+ title: 'Polymech RSS Feed',
+ description: '',
+}
+
+/////////////////////////////////////////////
+//
+// Defaults
+
+export const DEFAULT_IMAGE_URL = 'https://picsum.photos/640/640'
+
+export const default_image = () => {
+ return {
+ alt: 'none',
+ src: DEFAULT_IMAGE_URL,
+ thumb: DEFAULT_IMAGE_URL
+ }
+}
+
+
+
+/////////////////////////////////////////////
+//
+// Optimization
+
+export const O_IMAGE = IMAGE_PRESET[E_BROADBAND_SPEED.MEDIUM]
+export const IMAGE_SETTINGS =
+{
+ GALLERY: {
+ SHOW_TITLE: true,
+ SHOW_DESCRIPTION: false,
+ SIZES_THUMB: O_IMAGE.sizes_thumbs,
+ SIZES_LARGE: O_IMAGE.sizes_large,
+ SIZES_REGULAR: O_IMAGE.sizes
+ },
+ LIGHTBOX: {
+ SHOW_TITLE: true,
+ SHOW_DESCRIPTION: true,
+ SIZES_THUMB: O_IMAGE.sizes_thumbs,
+ SIZES_LARGE: O_IMAGE.sizes_large,
+ SIZES_REGULAR: O_IMAGE.sizes
+ }
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/menu.json b/packages/polymech/src/app/menu.json
new file mode 100644
index 0000000..2214f16
--- /dev/null
+++ b/packages/polymech/src/app/menu.json
@@ -0,0 +1,59 @@
+{
+ "main": [
+ {
+ "name": "Home",
+ "url": "/"
+ },
+ {
+ "name": "Products",
+ "url": "/products"
+ },
+ {
+ "name": "Pages",
+ "url": "",
+ "hasChildren": true,
+ "children": [
+ {
+ "name": "About",
+ "url": "/about"
+ },
+ {
+ "name": "Contact",
+ "url": "/contact"
+ },
+ {
+ "name": "404 Page",
+ "url": "/404"
+ }
+ ]
+ },
+ {
+ "name": "Contact",
+ "url": "/contact"
+ }
+ ],
+ "footer": [
+ {
+ "name": "About",
+ "url": "/about"
+ },
+ {
+ "name": "Products",
+ "url": "/products"
+ },
+ {
+ "name": "Contact",
+ "url": "/contact"
+ }
+ ],
+ "footerCopyright": [
+ {
+ "name": "Privacy & Policy",
+ "url": "/privacy-policy"
+ },
+ {
+ "name": "Terms of Service",
+ "url": "/terms-services"
+ }
+ ]
+}
diff --git a/packages/polymech/src/app/navigation.ts b/packages/polymech/src/app/navigation.ts
new file mode 100644
index 0000000..500f4a7
--- /dev/null
+++ b/packages/polymech/src/app/navigation.ts
@@ -0,0 +1,44 @@
+import { translate } from '@/base/i18n.js'
+import { I18N_SOURCE_LANGUAGE } from './config.js'
+import config from "./config.json" with { "type": "json" }
+import pMap from 'p-map'
+
+export const items = async (opts: { locale: string }) => {
+ const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, opts.locale)
+ return [
+ {
+ "href": `/${opts.locale}`,
+ "title": _T("Home"),
+ "ariaLabel": "Home",
+ "class": "hover:text-orange-600"
+ },
+ {
+ "href": `/resources/home`,
+ "title": _T("Resources"),
+ "ariaLabel": "Resources",
+ "class": "hover:text-orange-600"
+ }
+ ]
+}
+export const footer_left = async ( locale: string ) => {
+ const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, locale)
+ return await pMap(config.footer_left, async (item:any) => {
+ return {
+ "href": `${item.href}`,
+ "title": await _T(item.text),
+ "ariaLabel": item.text,
+ "class": "hover:text-orange-600"
+ }
+ });
+}
+export const footer_right = async ( locale: string ) => {
+ const _T = async (text: string) => await translate(text, I18N_SOURCE_LANGUAGE, locale)
+ return await pMap(config.footer_right, async (item:any) => {
+ return {
+ "href": `/${item.href}`,
+ "title": await _T(item.text),
+ "ariaLabel": item.text,
+ "class": "hover:text-orange-600"
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/network.ts b/packages/polymech/src/app/network.ts
new file mode 100644
index 0000000..4d69a80
--- /dev/null
+++ b/packages/polymech/src/app/network.ts
@@ -0,0 +1,49 @@
+import { z } from "zod"
+/////////////////////////////////////////////
+//
+// Optimizations
+
+// Image optimization (imagetools breakpoints & min widths)
+
+export enum E_BROADBAND_SPEED {
+ SLOW = "slow",
+ MEDIUM = "medium",
+ FAST = "fast",
+}
+
+const imageConfigSchema = z.object({
+ sizes: z.string(),
+ sizes_thumbs: z.string(),
+ sizes_large: z.string(),
+})
+const imagesSchema = z.object({
+ [E_BROADBAND_SPEED.SLOW]: imageConfigSchema,
+ [E_BROADBAND_SPEED.MEDIUM]: imageConfigSchema,
+ [E_BROADBAND_SPEED.FAST]: imageConfigSchema,
+});
+
+type Images = z.infer;
+
+export const IMAGE_PRESET: Images =
+{
+ [E_BROADBAND_SPEED.SLOW]: {
+ // For 2g connections: smaller image widths help performance. (Middle East & Africa)
+ sizes: "(min-width: 100px) 100px, 100vw",
+ sizes_thumbs: "(min-width: 80px) 80px, 80vw",
+ sizes_large: "(min-width: 320px) 320px, 320vw",
+ },
+ [E_BROADBAND_SPEED.MEDIUM]:
+ {
+ // For 3g connections: a moderate size image for a balance of quality and speed.
+ sizes: "(min-width: 800px) 800px, 800vw",
+ sizes_thumbs: "(min-width: 120px) 120px, 120vw",
+ sizes_large: "(min-width: 1024px) 1024px, 1024vw",
+ },
+ [E_BROADBAND_SPEED.FAST]:
+ {
+ // For 4g connections: larger images for high-resolution displays.
+ sizes: "(min-width: 1024px) 1024px, 1024vw",
+ sizes_thumbs: "(min-width: 180px) 180px, 180vw",
+ sizes_large: "(min-width: 1200px) 1200px, 1200vw"
+ }
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/profile.json b/packages/polymech/src/app/profile.json
new file mode 100644
index 0000000..dbc3ff9
--- /dev/null
+++ b/packages/polymech/src/app/profile.json
@@ -0,0 +1,43 @@
+{
+ "includes": [],
+ "variables": {
+ "PRODUCT_ROOT": "${root}/${product}/",
+ "abs_url": "https://assets.osr-plastic.org",
+ "CACHE": "${root}/cache/",
+ "CACHE_URL": "${abs_url}/cache/",
+ "GIT_REPO": "https://git.polymech.io/",
+ "OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org",
+ "PRODUCTS_ASSETS_URL":"https://assets.osr-plastic.org/${product_rel}",
+ "OSR_FILES_WEB":"https://files.polymech.io/files/machines",
+ "PRODUCTS_FILES_URL":"${OSR_FILES_WEB}/${product_rel}",
+ "DISCORD":"https://discord.gg/s8K7yKwBRc"
+ },
+ "env": {
+ "astro-release":{
+ "includes": [
+ "${PRODUCT_ROOT}"
+ ],
+ "variables": {
+ "OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org/"
+ }
+ },
+ "astro-debug":{
+ "includes": [
+ "${PRODUCT_ROOT}"
+ ],
+ "variables": {
+ "OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org",
+ "showCart": false,
+ "showPrice": false,
+ "showResources": false,
+ "showShipping": false,
+ "showPaymentTerms": false,
+ "showHowtos": false,
+ "showRenderings": true,
+ "debug": true
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/social.json b/packages/polymech/src/app/social.json
new file mode 100644
index 0000000..733bce2
--- /dev/null
+++ b/packages/polymech/src/app/social.json
@@ -0,0 +1,24 @@
+{
+ "main": [
+ {
+ "name": "facebook",
+ "icon": "FaFacebookF",
+ "link": "https://www.facebook.com/themefisher"
+ },
+ {
+ "name": "twitter",
+ "icon": "FaXTwitter",
+ "link": "https://x.com/themefisher"
+ },
+ {
+ "name": "linkedin",
+ "icon": "FaLinkedinIn",
+ "link": "https://bd.linkedin.com/company/themefisher"
+ },
+ {
+ "name": "github",
+ "icon": "FaGithub",
+ "link": "https://github.com/themefisher/astrofront"
+ }
+ ]
+}
diff --git a/packages/polymech/src/app/stores.json b/packages/polymech/src/app/stores.json
new file mode 100644
index 0000000..4a446b9
--- /dev/null
+++ b/packages/polymech/src/app/stores.json
@@ -0,0 +1,8 @@
+{
+ "shop":{
+ "title": "Shop",
+ "description": "",
+ "items":"${OSR_ROOT}/products/products/**/config.json",
+ "root":"${OSR_ROOT}/products"
+ }
+}
\ No newline at end of file
diff --git a/packages/polymech/src/app/theme.json b/packages/polymech/src/app/theme.json
new file mode 100644
index 0000000..98e2abd
--- /dev/null
+++ b/packages/polymech/src/app/theme.json
@@ -0,0 +1,44 @@
+{
+ "colors": {
+ "default": {
+ "theme_color": {
+ "primary": "#121212",
+ "body": "#fff",
+ "border": "#eaeaea",
+ "theme_light": "#f2f2f2",
+ "theme_dark": "#000"
+ },
+ "text_color": {
+ "default": "#444",
+ "dark": "#000",
+ "light": "#666"
+ }
+ },
+ "darkmode": {
+ "theme_color": {
+ "primary": "#fff",
+ "body": "#252525",
+ "border": "#3E3E3E",
+ "theme_light": "#222222",
+ "theme_dark": "#000"
+ },
+ "text_color": {
+ "default": "#DDD",
+ "dark": "#fff",
+ "light": "#DDD"
+ }
+ }
+ },
+ "fonts": {
+ "font_family": {
+ "primary": "Karla:wght@400;500;700",
+ "primary_type": "sans-serif",
+ "secondary": "",
+ "secondary_type": ""
+ },
+ "font_size": {
+ "base": "16",
+ "scale": "1.2"
+ }
+ }
+}
diff --git a/packages/polymech/src/base/i18n.ts b/packages/polymech/src/base/i18n.ts
index 95d91b3..9db29b0 100644
--- a/packages/polymech/src/base/i18n.ts
+++ b/packages/polymech/src/base/i18n.ts
@@ -7,10 +7,10 @@ import { CONFIG_DEFAULT } from '@polymech/commons'
import { I18N_ASSET_PATH, I18N_CACHE, I18N_SOURCE_LANGUAGE, PRODUCT_SPECS, RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS } from '@/app/config.js'
import { translateXLS } from '@polymech/i18n/translate_xls'
+import { I18N_STORE, OSR_ROOT } from 'config/config.js'
import { translateText } from '@polymech/i18n/translate_text'
import { logger } from './index.js'
-import { I18N_STORE, OSR_ROOT } from 'config/config.js'
export type { IOptions } from '@polymech/i18n'
export const translate = async (text: string, srcLanguage = 'en', targetLanguage, opts = {}) => {
diff --git a/packages/polymech/src/base/images.ts b/packages/polymech/src/base/images.ts
new file mode 100644
index 0000000..b71fc82
--- /dev/null
+++ b/packages/polymech/src/base/images.ts
@@ -0,0 +1,508 @@
+export interface ExifData {
+ name: string;
+ value: string;
+}
+
+export interface ImageSEOData {
+ src: string;
+ alt: string;
+ title: string;
+ caption?: string;
+ fileName: string;
+ format: string;
+ size: string;
+ metadata: {
+ location?: string;
+ camera?: string;
+ keywords: string[];
+ exifData: ExifData[];
+ };
+}
+
+export interface GalleryImage {
+ name?: string
+ url?: string
+ src: string
+ thumb?: string
+ responsive?: string
+ meta?: Meta
+ keywords?: string
+ description?: string
+ alt?: string
+ title?: string
+ height?: number
+ width?: number
+ gps?: { lon: number, lat: number }
+}
+
+export interface MetaJSON {
+ alt?: string,
+ keywords?: "",
+ title?: "",
+ description?: ""
+}
+export interface Meta {
+ format: string
+ width: number
+ height: number
+ space: string
+ channels: number
+ depth: string
+ density: number
+ chromaSubsampling: string
+ isProgressive: boolean
+ resolutionUnit: string
+ hasProfile: boolean
+ hasAlpha: boolean
+ orientation: number
+ exif: Exif
+ json: MetaJSON
+ markdown: string
+}
+
+export interface Exif {
+ file: File
+ jfif: Jfif
+ exif: Exif2
+ gps: Gps
+}
+
+export interface File {
+ "Bits Per Sample": BitsPerSample
+ "Image Height": ImageHeight
+ "Image Width": ImageWidth
+ "Color Components": ColorComponents
+ Subsampling: Subsampling
+ FileType: FileType
+}
+
+export interface BitsPerSample {
+ value: number
+ description: string
+}
+
+export interface ImageHeight {
+ value: number
+ description: string
+}
+
+export interface ImageWidth {
+ value: number
+ description: string
+}
+
+export interface ColorComponents {
+ value: number
+ description: string
+}
+
+export interface Subsampling {
+ description: string
+}
+
+export interface FileType {
+ value: string
+ description: string
+}
+
+export interface Jfif {
+ "JFIF Version": JfifVersion
+ "Resolution Unit": ResolutionUnit
+ XResolution: Xresolution
+ YResolution: Yresolution
+ "JFIF Thumbnail Width": JfifThumbnailWidth
+ "JFIF Thumbnail Height": JfifThumbnailHeight
+}
+
+export interface JfifVersion {
+ value: number
+ description: string
+}
+
+export interface ResolutionUnit {
+ value: number
+ description: string
+}
+
+export interface Xresolution {
+ value: number
+ description: string
+}
+
+export interface Yresolution {
+ value: number
+ description: string
+}
+
+export interface JfifThumbnailWidth {
+ value: number
+ description: string
+}
+
+export interface JfifThumbnailHeight {
+ value: number
+ description: string
+}
+
+export interface Exif2 {
+ ImageDescription: ImageDescription
+ Make: Make
+ Model: Model
+ Orientation: Orientation
+ XResolution: Xresolution2
+ YResolution: Yresolution2
+ ResolutionUnit: ResolutionUnit2
+ Software: Software
+ DateTime: DateTime
+ YCbCrPositioning: YcbCrPositioning
+ "Exif IFD Pointer": ExifIfdPointer
+ "GPS Info IFD Pointer": GpsInfoIfdPointer
+ XPTitle: Xptitle
+ XPSubject: Xpsubject
+ Padding: Padding
+ ExposureTime: ExposureTime
+ FNumber: Fnumber
+ ExposureProgram: ExposureProgram
+ ISOSpeedRatings: IsospeedRatings
+ ExifVersion: ExifVersion
+ DateTimeOriginal: DateTimeOriginal
+ DateTimeDigitized: DateTimeDigitized
+ ComponentsConfiguration: ComponentsConfiguration
+ ExposureBiasValue: ExposureBiasValue
+ MeteringMode: MeteringMode
+ LightSource: LightSource
+ Flash: Flash
+ FocalLength: FocalLength
+ SubSecTime: SubSecTime
+ SubSecTimeOriginal: SubSecTimeOriginal
+ SubSecTimeDigitized: SubSecTimeDigitized
+ FlashpixVersion: FlashpixVersion
+ ColorSpace: ColorSpace
+ PixelXDimension: PixelXdimension
+ PixelYDimension: PixelYdimension
+ ExposureMode: ExposureMode
+ WhiteBalance: WhiteBalance
+ DigitalZoomRatio: DigitalZoomRatio
+ FocalLengthIn35mmFilm: FocalLengthIn35mmFilm
+ SceneCaptureType: SceneCaptureType
+ GPSLatitudeRef: GpslatitudeRef
+ GPSLatitude: Gpslatitude
+ GPSLongitudeRef: GpslongitudeRef
+ GPSLongitude: Gpslongitude
+ GPSAltitude: Gpsaltitude
+}
+
+export interface ImageDescription {
+ id: number
+ description: string
+}
+
+export interface Make {
+ id: number
+ description: string
+}
+
+export interface Model {
+ id: number
+ description: string
+}
+
+export interface Orientation {
+ id: number
+ value: number
+ description: string
+}
+
+export interface Xresolution2 {
+ id: number
+ description: string
+}
+
+export interface Yresolution2 {
+ id: number
+ description: string
+}
+
+export interface ResolutionUnit2 {
+ id: number
+ value: number
+ description: string
+}
+
+export interface Software {
+ id: number
+ description: string
+}
+
+export interface DateTime {
+ id: number
+ description: string
+}
+
+export interface YcbCrPositioning {
+ id: number
+ value: number
+ description: string
+}
+
+export interface ExifIfdPointer {
+ id: number
+ value: number
+ description: number
+}
+
+export interface GpsInfoIfdPointer {
+ id: number
+ value: number
+ description: number
+}
+
+export interface Xptitle {
+ id: number
+ description: string
+}
+
+export interface Xpsubject {
+ id: number
+ description: string
+}
+
+export interface Padding {
+ id: number
+ description: string
+}
+
+export interface ExposureTime {
+ id: number
+ description: string
+}
+
+export interface Fnumber {
+ id: number
+ description: string
+}
+
+export interface ExposureProgram {
+ id: number
+ value: number
+ description: string
+}
+
+export interface IsospeedRatings {
+ id: number
+ value: number
+ description: number
+}
+
+export interface ExifVersion {
+ id: number
+ description: string
+}
+
+export interface DateTimeOriginal {
+ id: number
+ description: string
+}
+
+export interface DateTimeDigitized {
+ id: number
+ description: string
+}
+
+export interface ComponentsConfiguration {
+ id: number
+ description: string
+}
+
+export interface ExposureBiasValue {
+ id: number
+ description: string
+}
+
+export interface MeteringMode {
+ id: number
+ value: number
+ description: string
+}
+
+export interface LightSource {
+ id: number
+ value: number
+ description: string
+}
+
+export interface Flash {
+ id: number
+ value: number
+ description: string
+}
+
+export interface FocalLength {
+ id: number
+ description: string
+}
+
+export interface SubSecTime {
+ id: number
+ description: string
+}
+
+export interface SubSecTimeOriginal {
+ id: number
+ description: string
+}
+
+export interface SubSecTimeDigitized {
+ id: number
+ description: string
+}
+
+export interface FlashpixVersion {
+ id: number
+ description: string
+}
+
+export interface ColorSpace {
+ id: number
+ value: number
+ description: string
+}
+
+export interface PixelXdimension {
+ id: number
+ value: number
+ description: number
+}
+
+export interface PixelYdimension {
+ id: number
+ value: number
+ description: number
+}
+
+export interface ExposureMode {
+ id: number
+ value: number
+ description: string
+}
+
+export interface WhiteBalance {
+ id: number
+ value: number
+ description: string
+}
+
+export interface DigitalZoomRatio {
+ id: number
+ description: string
+}
+
+export interface FocalLengthIn35mmFilm {
+ id: number
+ value: number
+ description: string
+}
+
+export interface SceneCaptureType {
+ id: number
+ value: number
+ description: string
+}
+
+export interface GpslatitudeRef {
+ id: number
+ description: string
+}
+
+export interface Gpslatitude {
+ id: number
+ description: number
+}
+
+export interface GpslongitudeRef {
+ id: number
+ description: string
+}
+
+export interface Gpslongitude {
+ id: number
+ description: number
+}
+
+export interface Gpsaltitude {
+ id: number
+ description: string
+}
+
+export interface Gps {
+ Latitude: number
+ Longitude: number
+}
+
+export const generateDefaultImageJSONLD = (imageData: ImageSEOData) => {
+ return {
+ "@context": "https://schema.org",
+ "@type": "ImageObject",
+ "contentUrl": imageData.src,
+ "name": imageData.title,
+ "description": imageData.caption || imageData.alt,
+ "width": parseInt(imageData.size.split('x')[0]),
+ "height": parseInt(imageData.size.split('x')[1]),
+ "thumbnail": `https://example.com/thumbnails/${imageData.fileName}`,
+ "license": "https://example.com/license",
+ "acquireLicensePage": "https://example.com/buy-license",
+ "copyrightNotice": `© ${new Date().getFullYear()} Default Organization`,
+ "creator": {
+ "@type": "Person",
+ "name": "Default Creator Name"
+ },
+ "copyrightHolder": {
+ "@type": "Organization",
+ "name": "Default Organization Name"
+ },
+ "contentLocation": imageData.metadata.location || "Unknown location",
+ "datePublished": new Date().toISOString().split('T')[0],
+ "exifData": imageData.metadata.exifData.length > 0 ? imageData.metadata.exifData : [
+ {
+ "@type": "PropertyValue",
+ "name": "Camera",
+ "value": imageData.metadata.camera || "Unknown camera"
+ },
+ {
+ "@type": "PropertyValue",
+ "name": "Keywords",
+ "value": imageData.metadata.keywords.join(', ')
+ }
+ ]
+ };
+}
+
+// Example usage
+const imageData: ImageSEOData = {
+ src: "https://example.com/image.jpg",
+ alt: "A beautiful scenery",
+ title: "Beautiful Scenery",
+ caption: "A beautiful scenery with mountains and a lake.",
+ fileName: "scenery.jpg",
+ format: "image/jpeg",
+ size: "1200x800",
+ metadata: {
+ location: "Mountain Lake",
+ camera: "Canon EOS 5D Mark IV",
+ keywords: ["scenery", "mountain", "lake"],
+ exifData: [
+ {
+ name: "Exposure Time",
+ value: "1/659 sec."
+ },
+ {
+ name: "FNumber",
+ value: "f/4.0"
+ },
+ {
+ name: "ISO",
+ value: "100"
+ }
+ ]
+ }
+};
diff --git a/packages/polymech/src/base/index.ts b/packages/polymech/src/base/index.ts
index aadef7a..5dee470 100644
--- a/packages/polymech/src/base/index.ts
+++ b/packages/polymech/src/base/index.ts
@@ -8,7 +8,10 @@ import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
import { findUp } from 'find-up'
import { createLogger } from '@polymech/log'
import { parse, IProfile } from '@polymech/commons/profile'
-import { renderMarkup } from "@/model/component.js";
+
+import { translate } from "@/base/i18n.js"
+import { renderMarkup } from "@/model/component.js"
+
import {
LOGGING_NAMESPACE,
OSRL_ENV,
@@ -17,10 +20,10 @@ import {
I18N_SOURCE_LANGUAGE
} from 'config/config.js'
-import { translate } from "@/base/i18n.js"
-export const logger = createLogger(LOGGING_NAMESPACE)
-export const boot = () => logger.info('Astro is booting up')
+export const logger = createLogger('polymech-astro')
+
+export const boot = () => { logger.info('Astro is booting up') }
export const env = (item_rel: string = ""): IProfile => {
let default_profile: IProfile = {
includes: [],
diff --git a/packages/polymech/src/commons/media.ts b/packages/polymech/src/base/media.ts
similarity index 51%
rename from packages/polymech/src/commons/media.ts
rename to packages/polymech/src/base/media.ts
index aca0c45..951a29a 100644
--- a/packages/polymech/src/commons/media.ts
+++ b/packages/polymech/src/base/media.ts
@@ -1,34 +1,31 @@
import * as path from 'node:path'
import pMap from 'p-map'
+
import { GlobOptions } from 'glob'
+
import { sanitizeUri } from 'micromark-util-sanitize-uri'
import ExifReader from 'exifreader'
+
import { loadImage } from "imagetools/api"
-import { sanitizeFilename } from '@polymech/fs/utils'
+
import { resolve } from '@polymech/commons'
import { files } from '@polymech/commons'
import { sync as exists } from '@polymech/fs/exists'
import { sync as read } from '@polymech/fs/read'
+
import { logger } from '@/base/index.js'
+import { GalleryImage, MetaJSON } from '@/base/images.js'
+
+import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.js'
+import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL } from '../app/config.js'
+
+
import { env } from './index.js'
-import { GalleryImage, MetaJSON } from './images.js'
-import {
- removeArrayValues,
- removeArrays,
- removeBufferValues,
- removeEmptyObjects
-} from '@/base/objects.js'
+const IMAGES_GLOB = '*.+(JPG|jpg|png|PNG|gif)'
-import {
- ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT,
- DEFAULT_IMAGE_URL, ASSETS_LOCAL,
- ASSETS_GLOB
-} from 'config/config.js'
-
-export const default_sanitizer = (files:string[]) => files.map((f) => sanitizeFilename(f))
-
-export const default_sort = (files: string[]): string[] => {
+export const default_sort = (files: string[]): string[] =>
+{
const getSortableParts = (filename: string) => {
const baseName = path.parse(filename).name;
const match = baseName.match(/^(\d+)_?(.*)$/); // Match leading numbers
@@ -36,37 +33,21 @@ export const default_sort = (files: string[]): string[] => {
const textPart = match ? match[2] : baseName; // Extract text part
return { numPart, textPart };
- }
+ };
+
return files.sort((a, b) => {
- const { numPart: aNum, textPart: aText } = getSortableParts(a)
- const { numPart: bNum, textPart: bText } = getSortableParts(b)
+ const { numPart: aNum, textPart: aText } = getSortableParts(a);
+ const { numPart: bNum, textPart: bText } = getSortableParts(b);
+
if (!isNaN(aNum) && !isNaN(bNum)) {
- return aNum - bNum || aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' })
+ return aNum - bNum || aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' });
}
- return aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' })
- })
-}
-export const default_filter = async (url: string) => {
- try {
- const response = await fetch(url, { method: 'HEAD' })
- if (!response.ok) {
- logger.warn(`Image URL not found ${url}`)
- return false
- }
- } catch (error) {
- logger.warn(`Image URL not found ${url} : ${error.message}`)
- return false
- }
- return true
-}
-
-export const default_filter_locale = async (url: string) => {
- return url && exists(url)
+ return aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' });
+ });
}
export const image_url = async (src, fallback = DEFAULT_IMAGE_URL) => {
- if (exists(src)) return src
let safeSrc = src
try {
const response = await fetch(src, { method: 'HEAD' })
@@ -80,23 +61,57 @@ export const image_url = async (src, fallback = DEFAULT_IMAGE_URL) => {
return safeSrc
}
-export const gallery = async ( assetPath, item): Promise => {
-
+const default_image = () => {
+ const url = image_url(DEFAULT_IMAGE_URL)
+ return {
+ name: "none",
+ src: url,
+ meta: {
+ format: '',
+ width: 0,
+ height: 0,
+ space: '',
+ channels: 0,
+ depth: 0,
+ density: 0,
+ chromaSubsampling: '',
+ isProgressive: false,
+ resolutionUnit: 0,
+ hasProfile: false,
+ hasAlpha: false,
+ orientation: 0,
+ exif: {},
+ json: {},
+ markdown: ""
+ },
+ keywords: [],
+ description: '',
+ alt: '',
+ width: 0,
+ height: 0,
+ title: '',
+ gps: { lon: '', lat: '' }
+ }
+}
+export const gallery = async (
+ assetPath,
+ product): Promise => {
+ product = '' + product
const root = resolve(PRODUCT_ROOT())
const profile = env()
const assetSlug = path.parse(assetPath).name
- const itemConfig: any = read(PRODUCT_CONFIG(item), "json")
- if (!itemConfig) {
- logger.warn(`item gallery : item ${item} config not found !`)
- return []
+ const productConfig: any = read(PRODUCT_CONFIG(product), "json")
+ if (!productConfig) {
+ logger.warn(`ProductGallery : Product ${product} config not found !`)
+ return
}
- const mediaPath = `${root}/${item}/${assetPath}/`
+ const mediaPath = `${root}/${product}/${assetPath}/`
if (!exists(mediaPath)) {
- logger.warn(`item gallery : item ${item} media path not found ${mediaPath}!`)
+ logger.warn(`ProductGallery : Product ${product} media path not found ${mediaPath}!`)
return []
}
- const galleryGlob = (itemConfig.gallery || {})[assetSlug]?.glob || ASSETS_GLOB
+ const galleryGlob = (productConfig.gallery || {})[assetSlug]?.glob || IMAGES_GLOB
let galleryFiles: any[] = files(mediaPath, galleryGlob, {
cwd: mediaPath,
absolute: false,
@@ -108,35 +123,19 @@ export const gallery = async ( assetPath, item): Promise => {
}
if (!galleryFiles) {
- logger.warn(`gallery : ${item} media files not found ! ${mediaPath}`)
- return []
+ logger.warn(`ProductGallery : Product ${product} media files not found ! ${mediaPath}`)
+ return
}
- const assetUrl = (filePath) => {
- return sanitizeUri(ITEM_ASSET_URL(
- {
- assetPath,
- filePath,
- ITEM_REL: item,
- ...profile.variables
- }
- ))
- }
- if (!ASSETS_LOCAL) {
- galleryFiles = await pMap(galleryFiles, async (f) => (await default_filter(assetUrl(f))) ? f : null, { concurrency: 5 })
- galleryFiles = galleryFiles.filter((f) => f !== null)
- galleryFiles = default_sort(galleryFiles)
- } else {
- galleryFiles = galleryFiles.filter(default_filter_locale)
- }
galleryFiles = default_sort(galleryFiles)
-
return await pMap(galleryFiles, async (file: string) => {
+
const parts = path.parse(file)
const filePath = path.join(mediaPath, file)
const meta_path_json = `${mediaPath}/${parts.name}.json`
const meta_json = exists(meta_path_json) ? read(meta_path_json, "json") as MetaJSON : { alt: "", keywords: "", title: "", description: "" }
const meta_path_md = `${mediaPath}/${parts.name}.md`
const meta_markdown = exists(meta_path_md) ? read(meta_path_md, "string") as string : "" as string
+
let imageMeta: any = await loadImage(filePath)
let exifRaw: any = null
try {
@@ -145,7 +144,8 @@ export const gallery = async ( assetPath, item): Promise => {
logger.error(`ProductGallery : Error loading exif data for ${filePath}`)
exifRaw = {}
}
- const keywords = exifRaw?.['LastKeywordXMP']?.description || exifRaw?.iptc?.Keywords?.description || ''.split(',').map((k) => k.trim())
+
+ const keywords = exifRaw?.['LastKeywordXMP']?.description || exifRaw?.iptc?.Keywords?.description || ''
const exifDescription = exifRaw?.['ImageDescription']?.description || ''
const width = exifRaw?.['Image Width']?.value
const height = exifRaw?.['Image Height']?.value
@@ -153,7 +153,7 @@ export const gallery = async ( assetPath, item): Promise => {
const lat = exifRaw?.['GPSLatitude']?.description
const title = exifRaw?.title?.description || meta_json.title || ''
- const alt = meta_json.description || meta_markdown || exifDescription || exifRaw?.iptc?.['Caption/Abstract'].description || ''
+ const description = meta_json.description || meta_markdown || exifDescription || exifRaw?.iptc?.['Caption/Abstract'].description || ''
imageMeta.exif = exifRaw
imageMeta = removeBufferValues(imageMeta)
imageMeta = removeArrayValues(imageMeta)
@@ -164,12 +164,25 @@ export const gallery = async ( assetPath, item): Promise => {
delete imageMeta.exif.icc
delete imageMeta.exif.xmp
delete imageMeta.exif.iptc
- const src = ASSETS_LOCAL ? filePath : await image_url(assetUrl(file))
+ const keywordsTranslated = ''
+
+ const assetUrl = (filePath) => {
+ return sanitizeUri(ITEM_ASSET_URL(
+ {
+ assetPath,
+ filePath,
+ ITEM_REL: product,
+ ...profile.variables
+ }
+ ))
+ }
const ret: GalleryImage =
{
name: path.parse(file).name,
- src: src,
- url: src,
+ url: await image_url(assetUrl(file)),
+ src: await image_url(assetUrl(file)),
+ thumb: assetUrl(`/${parts.name}-thumb.webp`),
+ responsive: assetUrl(`/webp/${parts.name}.webp`),
meta: {
format: imageMeta.format,
width: imageMeta.width,
@@ -188,58 +201,15 @@ export const gallery = async ( assetPath, item): Promise => {
json: meta_json as MetaJSON,
markdown: meta_markdown as string
},
- keywords,
- description: alt,
- alt,
+ keywords: keywords.split(',').map((k) => k.trim()),
+ description,
+ alt: `${description} - ${keywordsTranslated || ''}`,
width,
height,
title,
gps: { lon, lat }
}
+
return ret
})
-}
-
-/**
- * Converts gallery images to individual JSON-LD objects for SEO optimization
- * @param images Array of GalleryImage objects to convert
- * @param lang Language code for internationalization
- * @param contentUrl Base URL for the gallery content
- * @returns Array of JSON-LD representations, one for each image
- */
-export const toJsonLd = async (images: GalleryImage[], lang: string, contentUrl: string) => {
- if (!images || images.length === 0) {
- return []
- }
-
- // Map each image to its own complete JSON-LD object
- return images.map((image, index) => {
- // Create a standalone ImageObject for each image
- const jsonLd = {
- "@context": "https://schema.org",
- "@type": "ImageObject",
- "inLanguage": lang,
- "contentUrl": contentUrl || image.src,
- "url": contentUrl || image.src,
- "name": image.title || image.name,
- "description": image.description || image.alt || "",
- "height": image.height,
- "width": image.width,
- "identifier": image.name,
- "keywords": Array.isArray(image.keywords) ? image.keywords.join(", ") : image.keywords || ""
- }
-
- // Add GPS coordinates if available
- if (image.gps && image.gps.lat && image.gps.lon) {
- jsonLd["contentLocation"] = {
- "@type": "Place",
- "geo": {
- "@type": "GeoCoordinates",
- "latitude": image.gps.lat,
- "longitude": image.gps.lon
- }
- }
- }
- return jsonLd
- })
-}
+}
\ No newline at end of file
diff --git a/packages/polymech/src/base/objects.ts b/packages/polymech/src/base/objects.ts
new file mode 100644
index 0000000..ba3148c
--- /dev/null
+++ b/packages/polymech/src/base/objects.ts
@@ -0,0 +1,69 @@
+///////////////////////////////////////////////
+//
+// trimming Exif data
+
+export const removeBufferValues = (obj: any): any => {
+ for (const key in obj) {
+ const val = obj[key]
+ if (Buffer.isBuffer(val)) {
+ }
+ if (Buffer.isBuffer(val)) {
+ delete obj[key];
+ } else if (typeof val === 'object') {
+ removeBufferValues(val);
+ }
+ }
+ return obj;
+}
+export const removeArrayValues = (obj: any): any => {
+ if (obj === null || obj === undefined) return obj;
+ for (const key in obj) {
+ const val = obj[key]
+ if (val === null || val === undefined) continue;
+ if (key == 'id') {
+ delete obj[key]
+ }
+ if (Array.isArray(val) || Buffer.isBuffer(val)) {
+ try {
+ delete obj[key];
+ } catch (e) {
+ debugger
+ }
+
+ } else if (typeof obj[key] === 'object') {
+ removeArrayValues(obj[key]);
+ }
+ }
+ return obj
+}
+export const removeEmptyObjects = (obj: any): any => {
+ if (obj === null || obj === undefined) return obj;
+ for (const key in obj) {
+ const val = obj[key]
+ if (val === null || val === undefined) continue;
+ if (typeof val === 'object' ||
+ (key == 'value' && typeof val === 'number' && val === 0 || key == 'base64')
+ ) {
+ obj[key] = removeEmptyObjects(obj[key]);
+ if (Object.keys(obj[key]).length === 0) {
+ delete obj[key];
+ }
+ }
+ }
+ return obj
+}
+export const removeArrays = (obj: any): any => {
+ for (const key in obj) {
+ if (key == 'description' && typeof obj[key] === 'string' && obj[key].split(',').length > 2) {
+ try {
+ if (Buffer.isBuffer(Buffer.from(obj[key].split(',').join(','))))
+ delete obj[key]
+ } catch (e) {
+
+ }
+ } else if (typeof obj[key] === 'object') {
+ removeArrays(obj[key]);
+ }
+ }
+ return obj
+}
\ No newline at end of file
diff --git a/packages/polymech/src/base/strings.ts b/packages/polymech/src/base/strings.ts
new file mode 100644
index 0000000..b4c61d0
--- /dev/null
+++ b/packages/polymech/src/base/strings.ts
@@ -0,0 +1,52 @@
+import { slug } from "github-slugger"
+import { marked } from "marked"
+export const slugify = (content: string) => slug(content)
+
+export const markdownify = (content: string, div?: boolean) => {
+ return div ? marked.parse(content) : marked.parseInline(content)
+}
+
+export const humanize = (content: string) => {
+ return content
+ .replace(/^[\s_]+|[\s_]+$/g, "")
+ .replace(/[_\s]+/g, " ")
+ .replace(/[-\s]+/g, " ")
+ .replace(/^[a-z]/, function (m) {
+ return m.toUpperCase();
+ })
+}
+
+export const titleify = (content: string) => {
+ const humanized = humanize(content);
+ return humanized
+ .split(" ")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ");
+}
+
+export const plainify = (content: string) => {
+ const parseMarkdown: any = marked.parse(content);
+ const filterBrackets = parseMarkdown.replace(/<\/?[^>]+(>|$)/gm, "");
+ const filterSpaces = filterBrackets.replace(/[\r\n]\s*[\r\n]/gm, "");
+ const stripHTML = htmlEntityDecoder(filterSpaces);
+ return stripHTML
+}
+
+// strip entities for plainify
+const htmlEntityDecoder = (htmlWithEntities: string) => {
+ let entityList: { [key: string]: string } = {
+ " ": " ",
+ "<": "<",
+ ">": ">",
+ "&": "&",
+ """: '"',
+ "'": "'",
+ };
+ let htmlWithoutEntities: string = htmlWithEntities.replace(
+ /(&|<|>|"|')/g,
+ (entity: string): string => {
+ return entityList[entity];
+ },
+ );
+ return htmlWithoutEntities;
+}
diff --git a/packages/polymech/src/commons/i18n.ts b/packages/polymech/src/commons/i18n.ts
deleted file mode 100644
index 853dbac..0000000
--- a/packages/polymech/src/commons/i18n.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import * as path from 'path'
-import { resolve } from '@polymech/commons'
-import { sync as exists } from '@polymech/fs/exists'
-
-import type { IOptions } from '@polymech/i18n'
-
-import { CONFIG_DEFAULT } from '@polymech/commons'
-import {
- I18N_ASSET_PATH,
- I18N_CACHE,
- I18N_SOURCE_LANGUAGE,
- PRODUCT_SPECS,
- RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS
-} from 'config/config.js'
-
-import { translateXLS } from '@polymech/i18n/translate_xls'
-import { I18N_STORE, OSR_ROOT } from 'config/config.js'
-import { translateText } from '@polymech/i18n/translate_text'
-import { logger } from './index.js'
-
-export type { IOptions } from '@polymech/i18n'
-
-export const translate = async (text: string, srcLanguage = 'en', targetLanguage, opts = {}) => {
- if (!targetLanguage) {
- return text
- }
- try {
- const store = I18N_STORE(OSR_ROOT(), targetLanguage)
- let translation = text
- translation = await translateText(text, srcLanguage, targetLanguage, {
- store,
- ...opts
- })
- return translation
- } catch (e) {
- logger.error(`Failed to translate text: ${text} from ${srcLanguage} to ${targetLanguage} : ${e.message}`)
- }
- return text
-}
-export const translateSheets = async (product, language) => {
- const config: any = CONFIG_DEFAULT()
- if (language === I18N_SOURCE_LANGUAGE) {
- return
- }
- const i18nOptions: IOptions = {
- srcLang: I18N_SOURCE_LANGUAGE,
- dstLang: language,
- src: PRODUCT_SPECS(product),
- store: I18N_STORE(OSR_ROOT(), language),
- dst: I18N_ASSET_PATH,
- query: "$[*][0,1,2,3]",
- cache: I18N_CACHE,
- api_key: config.deepl.auth_key,
- logLevel: RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS
- }
- const src = `${PRODUCT_SPECS(product)}`
- const srcParts = path.parse(src)
- const dst = path.resolve(resolve(I18N_ASSET_PATH, false, {
- SRC_DIR: srcParts.dir,
- SRC_NAME: srcParts.name,
- SRC_EXT: srcParts.ext,
- DST_LANG: language
- }))
- if (I18N_CACHE && exists(dst)) {
- return dst
- }
- logger.debug(`Translate assets ${src} to ${language}`)
- try {
- return await translateXLS(path.resolve(src), dst, i18nOptions)
- } catch (e) {
- logger.error(`Failed to translate assets ${src} to ${language}`, e.message)
- }
-}
diff --git a/packages/polymech/src/commons/index.ts b/packages/polymech/src/commons/index.ts
index b52d287..ce52c32 100644
--- a/packages/polymech/src/commons/index.ts
+++ b/packages/polymech/src/commons/index.ts
@@ -8,50 +8,3 @@ import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
import { findUp } from 'find-up'
import { createLogger } from '@polymech/log'
import { parse, IProfile } from '@polymech/commons/profile'
-
-import {
- LOGGING_NAMESPACE,
- OSRL_ENV,
- OSRL_PRODUCT_PROFILE,
- PRODUCT_ROOT
-} from 'config/config.js'
-
-export const logger = createLogger(LOGGING_NAMESPACE)
-export const boot = () => logger.info('Astro is booting up')
-export const env = (item_rel: string = ""): IProfile => {
- let default_profile: IProfile = {
- includes: [],
- variables: {
- root: PRODUCT_ROOT(),
- product: item_rel,
- product_rel: item_rel,
- }
- }
- default_profile = parse(OSRL_PRODUCT_PROFILE, default_profile, { env: OSRL_ENV })
- return default_profile;
-}
-export const render = async (string) => {
- const html = `${unescapeHTML(string)}`
- return createComponent(() => renderTemplate(html as any, []))
-}
-export const item_defaults = async (itemDir) => {
- return await findUp('defaults.json', {
- stopAt: PRODUCT_ROOT(),
- cwd: itemDir
- })
-}
-export async function markdownToHtml(markdown: string): Promise {
- const result = await unified()
- .use(remarkParse)
- .use(remarkRehype)
- .use(rehypeStringify)
- .process(markdown);
-
- return result.toString();
-}
-export const createMarkdownComponent = async (markdown: string) => {
- const html = unescapeHTML(await markdownToHtml(markdown));
- return createComponent(() => renderTemplate(html as any, []));
-}
-export const createHTMLComponent = async (html: string) =>
- createComponent(() => renderTemplate(unescapeHTML(html) as any, []))
\ No newline at end of file
diff --git a/packages/polymech/src/commons/seo.ts b/packages/polymech/src/commons/seo.ts
deleted file mode 100644
index b682310..0000000
--- a/packages/polymech/src/commons/seo.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-
-import { IComponentConfig } from '@polymech/commons'
-import { sync as read } from '@polymech/fs/read'
-import { I18N_SOURCE_LANGUAGE } from 'config/config.js'
-
-import { translate } from '@/base/i18n.js'
-import { item_defaults } from '@/base/index.js'
-
-import config from "../config/config.json" with { "type": "json" }
-
-const keywords = (keywords: string) => keywords.split(',').map(k => k.trim()).filter(Boolean);
-
-const unique = (...keywordGroups: string[][]) => {
- return Array.from(new Set(keywordGroups.flat()))
-};
-
-export const site_keywords = async (locale: string = I18N_SOURCE_LANGUAGE) => {
- const configKeywords = keywords(config.metadata.keywords || "");
- const allKeywords = unique(configKeywords)
- const system_keywords = await translate(allKeywords.join(','), I18N_SOURCE_LANGUAGE, locale);
- const keywordsArray = keywords(system_keywords)
- return keywordsArray.join(',');
-};
-
-export const item_keywords = async (item: IComponentConfig | null, locale: string = I18N_SOURCE_LANGUAGE) => {
- if (!item) {
- return (await site_keywords(locale))
- }
-
- let system_keywords = "";
- if (item.PRODUCT_ROOT) {
- const defaultsJson = await item_defaults(item.PRODUCT_ROOT);
- const defaults: Record = defaultsJson ? read(defaultsJson, 'json') as Record || {} : {}
- const defaultsKeywords = keywords(defaults.keywords || "")
- const configKeywords = await site_keywords(locale)
- const itemKeywords = keywords(item.keywords || "")
- const allKeywords = unique(defaultsKeywords, configKeywords.split(','), itemKeywords)
- system_keywords = await translate(allKeywords.join(','), I18N_SOURCE_LANGUAGE, locale)
- }
- const keywordsArray = unique([item.name], keywords(system_keywords))
- return keywordsArray.join(',')
-};
diff --git a/packages/polymech/src/commons/specs.ts b/packages/polymech/src/commons/specs.ts
deleted file mode 100644
index 0f56e51..0000000
--- a/packages/polymech/src/commons/specs.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import { parse } from 'node-xlsx'
-import { sync as read } from '@polymech/fs/read'
-import { sync as exists } from '@polymech/fs/exists'
-import pkg from 'showdown'
-const { Converter } = pkg
-
-export const md2html = (content) => {
- let converter = new Converter({ tables: true });
- converter.setOption('literalMidWordUnderscores', 'true');
- return converter.makeHtml(content);
-}
-
-/**
- * @typedef MarkdownTableOptions
- * @property {string|null|Array.} [align]
- * @property {boolean} [padding=true]
- * @property {boolean} [delimiterStart=true]
- * @property {boolean} [delimiterStart=true]
- * @property {boolean} [delimiterEnd=true]
- * @property {boolean} [alignDelimiters=true]
- * @property {(value: string) => number} [stringLength]
- */
-
-/**
- * Create a table from a matrix of strings.
- *
- * from : https://github.com/wooorm/markdown-table/blob/main/index.js
- *
- *
- *
- * @param {Array.>} table
- * @param {MarkdownTableOptions} [options]
- * @returns {string}
- */
-export const markdownTable = (table, options: any = {}) => {
-
- const align = (options.align || []).concat()
- const stringLength = options.stringLength || defaultStringLength
- /** @type {Array} Character codes as symbols for alignment per column. */
- const alignments = []
- /** @type {Array>} Cells per row. */
- const cellMatrix = []
- /** @type {Array>} Sizes of each cell per row. */
- const sizeMatrix = []
- /** @type {Array} */
- const longestCellByColumn = []
- let mostCellsPerRow = 0
- let rowIndex = -1
-
- // This is a superfluous loop if we don’t align delimiters, but otherwise we’d
- // do superfluous work when aligning, so optimize for aligning.
- while (++rowIndex < table.length) {
- /** @type {Array} */
- const row = []
- /** @type {Array} */
- const sizes = []
- let columnIndex = -1
-
- if (table[rowIndex].length > mostCellsPerRow) {
- mostCellsPerRow = table[rowIndex].length
- }
-
- while (++columnIndex < table[rowIndex].length) {
- const cell = serialize(table[rowIndex][columnIndex])
-
- if (options.alignDelimiters !== false) {
- const size = stringLength(cell)
- sizes[columnIndex] = size
-
- if (
- longestCellByColumn[columnIndex] === undefined ||
- size > longestCellByColumn[columnIndex]
- ) {
- longestCellByColumn[columnIndex] = size
- }
- }
-
- row.push(cell)
- }
-
- cellMatrix[rowIndex] = row
- sizeMatrix[rowIndex] = sizes
- }
-
- // Figure out which alignments to use.
- let columnIndex = -1
-
- if (typeof align === 'object' && 'length' in align) {
- while (++columnIndex < mostCellsPerRow) {
- alignments[columnIndex] = toAlignment(align[columnIndex])
- }
- } else {
- const code = toAlignment(align)
-
- while (++columnIndex < mostCellsPerRow) {
- alignments[columnIndex] = code
- }
- }
-
- // Inject the alignment row.
- columnIndex = -1
- /** @type {Array} */
- const row = []
- /** @type {Array} */
- const sizes = []
-
- while (++columnIndex < mostCellsPerRow) {
- const code = alignments[columnIndex]
- let before = ''
- let after = ''
-
- if (code === 99 /* `c` */) {
- before = ':'
- after = ':'
- } else if (code === 108 /* `l` */) {
- before = ':'
- } else if (code === 114 /* `r` */) {
- after = ':'
- }
-
- // There *must* be at least one hyphen-minus in each alignment cell.
- let size =
- options.alignDelimiters === false
- ? 1
- : Math.max(
- 1,
- longestCellByColumn[columnIndex] - before.length - after.length
- )
-
- const cell = before + '-'.repeat(size) + after
-
- if (options.alignDelimiters !== false) {
- size = before.length + size + after.length
-
- if (size > longestCellByColumn[columnIndex]) {
- longestCellByColumn[columnIndex] = size
- }
-
- sizes[columnIndex] = size
- }
-
- row[columnIndex] = cell
- }
-
- // Inject the alignment row.
- cellMatrix.splice(1, 0, row)
- sizeMatrix.splice(1, 0, sizes)
-
- rowIndex = -1
- /** @type {Array} */
- const lines = []
-
- while (++rowIndex < cellMatrix.length) {
- const row = cellMatrix[rowIndex]
- const sizes = sizeMatrix[rowIndex]
- columnIndex = -1
- /** @type {Array} */
- const line = []
-
- while (++columnIndex < mostCellsPerRow) {
- const cell = row[columnIndex] || ''
- let before = ''
- let after = ''
-
- if (options.alignDelimiters !== false) {
- const size =
- longestCellByColumn[columnIndex] - (sizes[columnIndex] || 0)
- const code = alignments[columnIndex]
-
- if (code === 114 /* `r` */) {
- before = ' '.repeat(size)
- } else if (code === 99 /* `c` */) {
- if (size % 2) {
- before = ' '.repeat(size / 2 + 0.5)
- after = ' '.repeat(size / 2 - 0.5)
- } else {
- before = ' '.repeat(size / 2)
- after = before
- }
- } else {
- after = ' '.repeat(size)
- }
- }
-
- if (options.delimiterStart !== false && !columnIndex) {
- line.push('|')
- }
-
- if (
- options.padding !== false &&
- // Don’t add the opening space if we’re not aligning and the cell is
- // empty: there will be a closing space.
- !(options.alignDelimiters === false && cell === '') &&
- (options.delimiterStart !== false || columnIndex)
- ) {
- line.push(' ')
- }
-
- if (options.alignDelimiters !== false) {
- line.push(before)
- }
-
- line.push(cell)
-
- if (options.alignDelimiters !== false) {
- line.push(after)
- }
-
- if (options.padding !== false) {
- line.push(' ')
- }
-
- if (
- options.delimiterEnd !== false ||
- columnIndex !== mostCellsPerRow - 1
- ) {
- line.push('|')
- }
- }
-
- lines.push(
- options.delimiterEnd === false
- ? line.join('').replace(/ +$/, '')
- : line.join('')
- )
- }
-
- return lines.join('\n')
-}
-
-/**
- * @param {string|null|undefined} [value]
- * @returns {string}
- */
-function serialize(value) {
- return value === null || value === undefined ? '' : String(value)
-}
-
-/**
- * @param {string} value
- * @returns {number}
- */
-function defaultStringLength(value) {
- return value.length
-}
-
-/**
- * @param {string|null|undefined} value
- * @returns {number}
- */
-function toAlignment(value) {
- const code = typeof value === 'string' ? value.codePointAt(0) : 0
-
- return code === 67 /* `C` */ || code === 99 /* `c` */
- ? 99 /* `c` */
- : code === 76 /* `L` */ || code === 108 /* `l` */
- ? 108 /* `l` */
- : code === 82 /* `R` */ || code === 114 /* `r` */
- ? 114 /* `r` */
- : 0
-}
-
-
-export const specs = (path: string) => {
- if (!path || !exists(path)) {
- return '';
- } else {
- let data = parse(path) as any;
- data[0].data = data[0].data.filter((d) => !!d.length);
- data = markdownTable(data[0].data);
- const ret = md2html(data);
- return ret
- }
-}
diff --git a/packages/polymech/src/components/i18n.astro b/packages/polymech/src/components/i18n.astro
deleted file mode 100644
index 80eebfc..0000000
--- a/packages/polymech/src/components/i18n.astro
+++ /dev/null
@@ -1,22 +0,0 @@
----
-import { I18N_SOURCE_LANGUAGE } from "config/config.js"
-import { translate, IOptions } from '@/base/i18n.js'
-
-export interface Props extends IOptions {
- language?: string,
- clazz?:string
-}
-
-const {
- language = Astro.currentLocale,
- clazz = '',
- ...rest
-} = Astro.props
-
-const content = await Astro.slots.render('default')
-const translatedText = await translate(content, I18N_SOURCE_LANGUAGE, language, rest)
-
----
-
- {translatedText}
-
diff --git a/packages/polymech/src/components/test.astro b/packages/polymech/src/components/test.astro
new file mode 100644
index 0000000..6d414ce
--- /dev/null
+++ b/packages/polymech/src/components/test.astro
@@ -0,0 +1,10 @@
+---
+// Remove the problematic import for now since it's not used
+// import { translate, IOptions } from '@/base/i18n.js'
+
+const foo = () => 'bar'
+
+---
+
+ {foo()}
+
diff --git a/packages/polymech/index.ts b/packages/polymech/src/index.ts
similarity index 66%
rename from packages/polymech/index.ts
rename to packages/polymech/src/index.ts
index 50ba8f2..56bbb1c 100644
--- a/packages/polymech/index.ts
+++ b/packages/polymech/src/index.ts
@@ -1,4 +1,9 @@
-export const foo = 2
+export const foo2 = 2
// export { default as Gallery } from './components/Gallery.astro'
// export { default as AI } from './components/kbot.astro'
// export { default as i18n } from './components/i18n.astro'
+// export { default as Test } from './components/test.astro'
+
+
+
+
diff --git a/packages/polymech/src/model/component.ts b/packages/polymech/src/model/component.ts
new file mode 100644
index 0000000..69ca209
--- /dev/null
+++ b/packages/polymech/src/model/component.ts
@@ -0,0 +1,268 @@
+import * as path from 'path'
+import { findUp } from 'find-up'
+
+
+
+import { sync as read } from '@polymech/fs/read'
+import { sync as exists } from '@polymech/fs/exists'
+
+import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
+import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
+
+import { ContentEntryRenderFunction, ContentEntryType } from 'astro'
+import { RenderedContent, DataEntry } from "astro:content"
+import type { Loader, LoaderContext } from 'astro/loaders'
+
+import {
+ CAD_MAIN_MATCH, PRODUCT_BRANCHES,
+ CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
+ PRODUCT_ROOT, RETAIL_PRODUCT_BRANCH, CAD_EXPORT_CONFIGURATIONS,
+ CAD_DEFAULT_CONFIGURATION,
+ CAD_URL,
+ parseBoolean
+} from 'config/config.js'
+
+import { env } from '../base/index.js'
+import { gallery } from '@/base/media.js';
+
+import { get } from '@polymech/commons/component'
+import { PFilterValid } from '@polymech/commons/filter'
+
+import { IAssemblyData} from '@polymech/cad'
+import { logger as log } from '@/base/index.js'
+
+interface ILoaderContextEx extends LoaderContext {
+ entryTypes: Map
+}
+interface IComponentConfigEx extends IComponentConfig {
+ content: string
+ extra_resources?: string
+ shared_resources?: string
+ readme?: string
+ rel: string
+}
+export interface IStoreItem extends DataEntry {
+ data: IComponentConfigEx
+ rendered?: RenderedContent
+}
+
+let loaderCtx: ILoaderContextEx
+
+const renderFunctionByContentType = new WeakMap();
+
+const filterBranch = (items: { rel: string, config, path }[],
+ branch: string = RETAIL_PRODUCT_BRANCH) => {
+ if (!PRODUCT_BRANCHES) {
+ return items
+ }
+ const branchItems = PRODUCT_BRANCHES[branch]
+ if (!branchItems) {
+ return items
+ }
+ return items.filter((item) => branchItems.includes(item.rel))
+}
+
+export const items = (opts: {}) => filterBranch(get(`${PRODUCT_ROOT()}/${PRODUCT_GLOB}`, PRODUCT_ROOT(), PFilterValid.marketplace_component))
+
+
+const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
+ /*
+ const onNode = async (data: INodeCallback, configuration: string) => {
+ if (!CAD_EXPORT_SUB_COMPONENTS || !data.target.endsWith('.json')) {
+ return
+ }
+ const modelPath = `${CAD_MODEL_FILE_PATH(data.target,configuration)}`
+ const model: IAssemblyData = read(modelPath, 'json') as IAssemblyData
+ if (!model) {
+ return
+ }
+ const configurations = Object.keys(model.Configurations).filter((c) => {
+ return c !== CAD_DEFAULT_CONFIGURATION &&
+ c !== 'Global' &&
+ model.Configurations[c].Hide !== '1'
+ })
+ if (!configurations.length ||
+ model.Configurations?.Global?.['Configurations'] !== '1') {
+ return
+ }
+ }
+ */
+}
+
+export const getRenderFunction = async (fileName: string) => {
+ let entryType = loaderCtx.entryTypes.get(path.parse(fileName).ext) as ContentEntryType
+ let _render = renderFunctionByContentType.get(entryType) as ContentEntryRenderFunction
+
+ if (!_render) {
+ _render = await (entryType as any).getRenderFunction({})
+ renderFunctionByContentType.set(entryType, _render)
+ }
+ return _render
+}
+export const renderMarkup = async (content, data: any, fileName: string = 'template.md') => {
+ if (!loaderCtx) {
+ debugger
+ log.error('Loader context not set')
+ return
+ }
+ const _render = await getRenderFunction(fileName)
+ if (!_render) {
+ log.error('No render function')
+ return
+ }
+ return _render({
+ body: content,
+ data,
+ filePath: fileName,
+ } as any)
+}
+const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
+ if (!item || !item.data) {
+ ctx.logger.error(`Error completing ${''}: no data`);
+ return
+ }
+ if(!loaderCtx){
+ loaderCtx = ctx
+ }
+ const { logger } = ctx
+ let data: IComponentConfigEx = item.data
+
+ const itemRel = data.rel
+ const itemRelMin = data.rel.replace('products/', '')
+ const itemDir = PRODUCT_DIR(itemRel)
+ const default_profile = env(itemRel)
+
+ data.product_rel = itemRelMin
+ data.assets = {
+ renderings: [],
+ gallery: []
+ }
+ //////////////////////////////////////////
+ //
+ // Body
+ //
+ let contentPath = path.join(itemDir, 'templates/shared', 'body.md')
+ await getRenderFunction(contentPath)
+ if (exists(contentPath)) {
+ data.content = read(contentPath) as string
+ (item as any).filePath = contentPath
+ }
+ //////////////////////////////////////////
+ //
+ // Item Extra Resources
+ //
+ let resourcesPath = path.join(itemDir, 'templates/shared', 'resources.md')
+ exists(resourcesPath) && (data.extra_resources = read(resourcesPath) as string || "")
+ //////////////////////////////////////////
+ //
+ // Item Shared Resources
+ //
+ let resourcesDefaultPath = await findUp('resources.md', {
+ stopAt: PRODUCT_ROOT(),
+ cwd: itemDir
+ }) || ""
+ exists(resourcesDefaultPath) && (data.shared_resources = read(resourcesDefaultPath) as string || "")
+ //////////////////////////////////////////
+ //
+ // Readme
+ //
+ let readmePath = path.join(itemDir, 'Readme.md')
+ if (exists(readmePath)) {
+ data.readme = read(readmePath) as string
+ }
+ //////////////////////////////////////////
+ //
+ // Variables
+ //
+ let defaultsJSON = await findUp('defaults.json', {
+ stopAt: PRODUCT_ROOT(),
+ cwd: itemDir
+ })
+ try {
+ if (defaultsJSON) {
+ data = {
+ ...read(defaultsJSON, 'json') as any,
+ ...data,
+ }
+ }
+ } catch (error) {
+ logger.error(`Error reading defaults.json: ${error.message}`);
+ }
+
+ data = {
+ ...data,
+ ...default_profile.variables,
+ product_rel_min: itemRelMin.replace('products/', ''),
+ }
+ data = resolveConfig(data as Record) as IComponentConfigEx
+ item.data = data
+ //////////////////////////////////////////
+ //
+ // Extensions, CAD, Media, etc.
+ //
+
+ data.assets.renderings = await gallery('renderings', data.rel) as []
+ data.assets.renderings.length && (data.thumbnail =
+ {
+ alt: '',
+ url: data.assets.renderings[0].thumb,
+ src: data.assets.renderings[0].thumb
+ })
+ data.assets.gallery = await gallery('media/gallery', data.rel) as []
+ data.image = data.assets.renderings[0] || {}
+
+ data.assets.showcase = await gallery('media/showcase', data.rel) as []
+ data.assets.samples = await gallery('media/samples', data.rel) as []
+
+}
+export function loader(): Loader {
+ const load = async ({
+ config,
+ logger,
+ watcher,
+ parseData,
+ store,
+ generateDigest,
+ entryTypes }: ILoaderContextEx) => {
+
+ store.clear();
+ let products = items({})
+ for (const item of products) {
+ const product: any = item.config
+ const id = product.slug;
+ const data = {
+ rel: item.rel,
+ title: product.name,
+ slug: id,
+ type: 'product',
+ highlights: [],
+ components: [],
+ ...product
+ }
+ //const parsedData = await parseData({ id, data: data });
+ const storeItem = {
+ digest: await generateDigest(data),
+ filePath: id,
+ assetImports: [],
+ id: `${item.rel}`,
+ data: data
+ }
+ await onItem(storeItem, {
+ logger,
+ watcher,
+ parseData,
+ store,
+ generateDigest,
+ entryTypes
+ } as any)
+ storeItem.data['config'] = JSON.stringify({
+ ...storeItem.data
+ }, null, 2)
+ store.set(storeItem)
+ }
+ }
+ return {
+ name: "store-loader",
+ load
+ };
+}
\ No newline at end of file
diff --git a/packages/polymech/tsconfig.json b/packages/polymech/tsconfig.json
index 0827fb6..8f8a4cb 100644
--- a/packages/polymech/tsconfig.json
+++ b/packages/polymech/tsconfig.json
@@ -23,4 +23,7 @@
}
},
"include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro"],
+ "files": [
+ "src/index.ts"
+ ]
}