basic shit

This commit is contained in:
babayaga 2025-08-15 23:28:38 +02:00
parent 745d1061df
commit 9a6fc75b4c
27 changed files with 1587 additions and 612 deletions

View File

@ -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)
---
<div data-widget="polymech.i18n" class={clazz}>
{translatedText}
</div>

View File

@ -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"
}
}

View File

@ -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)
}

View File

@ -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"
}
}
}
}

View File

@ -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<string, string>) =>
sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/${file}", { file, ...variables }))
export const ASSET_URL = (file: string, variables: Record<string, string>) =>
sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/products/${product_rel_min}/${file}", { file, ...variables }))
export const ITEM_ASSET_URL = (variables: Record<string, string>) =>
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
}
}

View File

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

View File

@ -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"
}
});
}

View File

@ -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<typeof imagesSchema>;
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"
}
}

View File

@ -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
}
}
}
}

View File

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

View File

@ -0,0 +1,8 @@
{
"shop":{
"title": "Shop",
"description": "",
"items":"${OSR_ROOT}/products/products/**/config.json",
"root":"${OSR_ROOT}/products"
}
}

View File

@ -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"
}
}
}

View File

@ -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 = {}) => {

View File

@ -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"
}
]
}
};

View File

@ -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: [],

View File

@ -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<GalleryImage[]> => {
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<GalleryImage[] | undefined> => {
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<GalleryImage[]> => {
}
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<GalleryImage[]> => {
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<GalleryImage[]> => {
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<GalleryImage[]> => {
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<GalleryImage[]> => {
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
})
}
}

View File

@ -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
}

View File

@ -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 } = {
"&nbsp;": " ",
"&lt;": "<",
"&gt;": ">",
"&amp;": "&",
"&quot;": '"',
"&#39;": "'",
};
let htmlWithoutEntities: string = htmlWithEntities.replace(
/(&amp;|&lt;|&gt;|&quot;|&#39;)/g,
(entity: string): string => {
return entityList[entity];
},
);
return htmlWithoutEntities;
}

View File

@ -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)
}
}

View File

@ -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<string> {
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, []))

View File

@ -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<string, string> = defaultsJson ? read(defaultsJson, 'json') as Record<string, string> || {} : {}
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(',')
};

View File

@ -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.<string|null|undefined>} [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.<Array.<string|null|undefined>>} table
* @param {MarkdownTableOptions} [options]
* @returns {string}
*/
export const markdownTable = (table, options: any = {}) => {
const align = (options.align || []).concat()
const stringLength = options.stringLength || defaultStringLength
/** @type {Array<number>} Character codes as symbols for alignment per column. */
const alignments = []
/** @type {Array<Array<string>>} Cells per row. */
const cellMatrix = []
/** @type {Array<Array<number>>} Sizes of each cell per row. */
const sizeMatrix = []
/** @type {Array<number>} */
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<string>} */
const row = []
/** @type {Array<number>} */
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<string>} */
const row = []
/** @type {Array<number>} */
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<string>} */
const lines = []
while (++rowIndex < cellMatrix.length) {
const row = cellMatrix[rowIndex]
const sizes = sizeMatrix[rowIndex]
columnIndex = -1
/** @type {Array<string>} */
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
}
}

View File

@ -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)
---
<div data-widget="polymech.i18n" class={clazz}>
{translatedText}
</div>

View File

@ -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'
---
<div data-widget="polymech.test">
{foo()}
</div>

View File

@ -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'

View File

@ -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<string, ContentEntryType>
}
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<ContentEntryType, ContentEntryRenderFunction>();
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<string, string>) 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
};
}

View File

@ -23,4 +23,7 @@
}
},
"include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro"],
"files": [
"src/index.ts"
]
}