basic shit
This commit is contained in:
parent
745d1061df
commit
9a6fc75b4c
@ -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>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
9
packages/polymech/src/app/cli.ts
Normal file
9
packages/polymech/src/app/cli.ts
Normal 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)
|
||||
}
|
||||
112
packages/polymech/src/app/config.json
Normal file
112
packages/polymech/src/app/config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
168
packages/polymech/src/app/config.ts
Normal file
168
packages/polymech/src/app/config.ts
Normal 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
|
||||
}
|
||||
}
|
||||
59
packages/polymech/src/app/menu.json
Normal file
59
packages/polymech/src/app/menu.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
packages/polymech/src/app/navigation.ts
Normal file
44
packages/polymech/src/app/navigation.ts
Normal 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"
|
||||
}
|
||||
});
|
||||
}
|
||||
49
packages/polymech/src/app/network.ts
Normal file
49
packages/polymech/src/app/network.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
43
packages/polymech/src/app/profile.json
Normal file
43
packages/polymech/src/app/profile.json
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
24
packages/polymech/src/app/social.json
Normal file
24
packages/polymech/src/app/social.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
packages/polymech/src/app/stores.json
Normal file
8
packages/polymech/src/app/stores.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"shop":{
|
||||
"title": "Shop",
|
||||
"description": "",
|
||||
"items":"${OSR_ROOT}/products/products/**/config.json",
|
||||
"root":"${OSR_ROOT}/products"
|
||||
}
|
||||
}
|
||||
44
packages/polymech/src/app/theme.json
Normal file
44
packages/polymech/src/app/theme.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 = {}) => {
|
||||
|
||||
508
packages/polymech/src/base/images.ts
Normal file
508
packages/polymech/src/base/images.ts
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@ -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: [],
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
69
packages/polymech/src/base/objects.ts
Normal file
69
packages/polymech/src/base/objects.ts
Normal 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
|
||||
}
|
||||
52
packages/polymech/src/base/strings.ts
Normal file
52
packages/polymech/src/base/strings.ts
Normal 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 } = {
|
||||
" ": " ",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
""": '"',
|
||||
"'": "'",
|
||||
};
|
||||
let htmlWithoutEntities: string = htmlWithEntities.replace(
|
||||
/(&|<|>|"|')/g,
|
||||
(entity: string): string => {
|
||||
return entityList[entity];
|
||||
},
|
||||
);
|
||||
return htmlWithoutEntities;
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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, []))
|
||||
@ -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(',')
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
10
packages/polymech/src/components/test.astro
Normal file
10
packages/polymech/src/components/test.astro
Normal 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>
|
||||
@ -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'
|
||||
|
||||
|
||||
|
||||
|
||||
268
packages/polymech/src/model/component.ts
Normal file
268
packages/polymech/src/model/component.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@ -23,4 +23,7 @@
|
||||
}
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro"],
|
||||
"files": [
|
||||
"src/index.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user