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" + ] }