stuff like that :)

This commit is contained in:
lovebird 2025-03-20 08:56:01 +01:00
parent 7bc95ea8b9
commit 97eb1c5e8a
8 changed files with 384 additions and 163 deletions

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-03-19T22:56:22.498Z"
"default": "2025-03-20T07:39:49.013Z"
},
"description": {
"type": "string",

File diff suppressed because one or more lines are too long

29
dev.code-workspace Normal file
View File

@ -0,0 +1,29 @@
{
"folders": [
{
"path": "."
},
{
"path": "../astro-components"
},
{
"path": "../polymech-mono/packages/commons"
}
],
"settings": {
"files.associations": {
"*.json.liquid": "json",
"*.yaml.liquid": "yaml",
"*.md.liquid": "markdown",
"*.js.liquid": "liquid-javascript",
"*.css.liquid": "liquid-css",
"*.scss.liquid": "liquid-scss",
"*.embeddedhtml": "html",
"*.cps": "javascript",
"*.h": "c",
"XASM2INT.C": "cpp",
"XASM.H": "cpp",
"TNC_f1.H": "cpp"
}
}
}

View File

@ -18,10 +18,19 @@ export const I18N_SOURCE_LANGUAGE = 'en'
export const I18N_CACHE = true
export const I18N_ASSET_PATH = "${SRC_DIR}/${SRC_NAME}-${DST_LANG}${SRC_EXT}"
// Library - Howtos
// Products
export const HOWTO_ROOT = () => path.resolve(resolve("./public/resources/howtos"))
export const HOWTO_GLOB = '**/config.json'
// 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'
export const ENABLED_PRODUCTS = "${OSR_ROOT}/products/config/machines.json"
export const PRODUCT_SPECS = (rel) => `${PRODUCT_ROOT()}/${rel}/specs.xlsx`
// Product compiler
export const PRODUCT_CONFIG = (product) =>
@ -35,9 +44,9 @@ export const PRODUCTS_TARGET_SRC = './src/content/en/retail'
export const PRODUCTS_TARGET = (lang) => `./content/${lang}/products`
// Product assets
export const ASSETS_LOCAL = false
export const ASSETS_GLOB = '*.+(JPG|jpg|jpeg|png|PNG|gif)'
// OSRL - Language
export const IS_DEV = true
export const OSRL_ENV = 'astro-release'
@ -47,10 +56,6 @@ export const OSRL_MODULE_NAME = 'polymech.io'
export const OSRL_PRODUCT_PROFILE = './src/config/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/'
@ -105,11 +110,11 @@ export const parseBoolean = (value: string): boolean => {
}
/////////////////////////////////////////////
//
// Rendering
// Rendering - Store
export const SHOW_DESCRIPTION = false
export const SHOW_LICENSE = true
export const SHOW_RENDERINGS = true
export const SHOW_TABS = false
export const SHOW_GALLERY = true
export const SHOW_FILES = true

36
src/model/commons.ts Normal file
View File

@ -0,0 +1,36 @@
import * as path from 'path'
import { z } from "zod"
import normalizeUrl from 'normalize-url'
import { PFilterValid } from '@polymech/commons/filter'
import { forward_slash, pathInfoEx, resolve, readOSRConfig, } from '@polymech/commons'
export const find_items = (nodes: string[], options) => {
nodes = nodes.filter(options.filter)
return nodes.map((c) => {
const root = resolve(options.root, false, {})
return {
rel: forward_slash(`${path.relative(root, path.parse(c).dir)}`),
path: forward_slash(`${options.root}/${path.relative(root, c)}`),
config: readOSRConfig(c)
}
})
}
export const get = (src, root, type) => {
const srcInfo = pathInfoEx(src, false, { absolute: true})
return srcInfo
/*
switch (type) {
case "howtos": {
const options = {
filter: "howtos"
root
}
return find_items(srcInfo.FILES, options)
}
}
*/
}

View File

@ -1,28 +1,27 @@
import * as path from 'path'
import { findUp } from 'find-up'
import { } from 'astro:content'
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,
PRODUCT_ROOT, CAD_EXPORT_CONFIGURATIONS,
CAD_DEFAULT_CONFIGURATION,
CAD_URL,
parseBoolean
parseBoolean,
DEFAULT_CONTACT,
default_image
} from 'config/config.js'
import { env } from '../base/index.js'
import { gallery } from '../base/media.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'
@ -33,9 +32,8 @@ import { translate } from "@/base/i18n.js"
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
import { slugify } from "@/base/strings.js"
export interface ILoaderContextEx extends LoaderContext {
entryTypes: Map<string, ContentEntryType>
}
export const ITEM_TYPE = 'product'
export interface IComponentConfigEx extends IComponentConfig {
content: string
extra_resources?: string
@ -48,10 +46,6 @@ export interface IStoreItem extends DataEntry {
rendered?: RenderedContent
}
let loaderCtx: ILoaderContextEx
const renderFunctionByContentType = new WeakMap<ContentEntryType, ContentEntryRenderFunction>();
const filterBranch = (items: { rel: string, config, path }[], branch: string) => {
if (!PRODUCT_BRANCHES) {
return items
@ -68,7 +62,7 @@ export const items = (branch: string) =>
PRODUCT_ROOT(), PFilterValid.marketplace_component), branch)
const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
const onComponent = async (item: IStoreItem, ctx: LoaderContext) => {
/*
const onNode = async (data: INodeCallback, configuration: string) => {
if (!CAD_EXPORT_SUB_COMPONENTS || !data.target.endsWith('.json')) {
@ -91,8 +85,25 @@ const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
}
*/
}
export const defaults = async (data: IComponentConfigEx, cwd: string, root:string): Promise<IComponentConfigEx> => {
let defaultsJSON = await findUp('defaults.json', {
stopAt: root,
cwd: cwd
});
try {
if (defaultsJSON) {
data = {
...read(defaultsJSON, 'json') as any,
...data,
};
}
} catch (error) {
}
return data;
};
const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSchema[]> => {
const cad = async (item: IStoreItem): Promise<ICADNodeSchema[]> => {
const root = PRODUCT_ROOT()
const data: IComponentConfigEx = item.data
const itemRel = data.rel
@ -136,58 +147,24 @@ const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSch
return cadMeta
}
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) => {
const onItem = async (item: IStoreItem, ctx: LoaderContext) => {
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 itemRelMin = itemRel.replace('products/', '')
const itemDir = PRODUCT_DIR(itemRel)
const default_profile = env(itemRel)
data.product_rel = itemRelMin
data.assets = {
renderings: [],
gallery: []
}
data.body = (read(path.join(itemDir, 'templates/shared', 'body.md')) as string) || ""
data.resources = (read(path.join(itemDir, 'templates/shared', 'resources.md')) as string) || ""
//////////////////////////////////////////
//
// Item Shared Resources
@ -207,45 +184,26 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
//
// 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 = await defaults(data, itemDir, PRODUCT_ROOT());
data = {
...data,
...default_profile.variables,
product_rel_min: itemRelMin.replace('products/', ''),
product_rel_min: itemRelMin,
}
data = resolveConfig(data as Record<string, string>) as IComponentConfigEx
item.data = data
//////////////////////////////////////////
//
// Extensions, CAD, Media, etc.
// Extensions, CAD, Media, etcetera etcetera :)
//
data.cad = await cad(item, ctx)
data.cad = await cad(item)
data.assets.renderings = await gallery('renderings', data.rel) as []
data.assets.renderings.length && (data.thumbnail =
{
alt: data.assets.renderings[0].alt,
url: data.assets.renderings[0].url,
src: data.assets.renderings[0].url
})
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 []
data.assets.renderings = await gallery('renderings', data.rel)
data.assets.gallery = await gallery('media/gallery', data.rel)
data.image = data.assets.renderings[0] || default_image()
data.assets.showcase = await gallery('media/showcase', data.rel)
data.assets.samples = await gallery('media/samples', data.rel)
data.checkout = await item_checkout(data)
}
export function loader(branch: string): Loader {
@ -255,8 +213,7 @@ export function loader(branch: string): Loader {
watcher,
parseData,
store,
generateDigest,
entryTypes }: ILoaderContextEx) => {
generateDigest }: LoaderContext) => {
store.clear();
let products = items(branch)
@ -268,7 +225,7 @@ export function loader(branch: string): Loader {
slug: id,
id,
title: product.name,
type: 'product',
type: ITEM_TYPE,
highlights: [],
components: [],
...product,
@ -286,25 +243,23 @@ export function loader(branch: string): Loader {
watcher,
parseData,
store,
generateDigest,
entryTypes
generateDigest
} as any)
storeItem.data['config'] = JSON.stringify({
...storeItem.data
}, null, 2)
storeItem.data['config'] = JSON.stringify(storeItem.data, null, 2)
store.set(storeItem)
}
}
return {
name: "store-loader",
name: `astro:store:${ITEM_TYPE}`,
load
};
}
export const group_path = (item) => item.id.split("/")[1]
const group_label = async (text: string, locale) => await translate(slugify(text), I18N_SOURCE_LANGUAGE, locale)
const group = async (items, locale) => {
return items.reduce(async (accPromise, item) => {
const acc = await accPromise
@ -319,4 +274,13 @@ const group = async (items, locale) => {
}, {})
}
export const group_by_path = async (items, locale): Promise<IComponentConfigEx[]> => await group(items, locale)
export const group_by_path = async (items, locale): Promise<IComponentConfigEx[]> => await group(items, locale)
export const mailto = (to: string, subject: string, body: string): string => {
const encode = (str: string) => encodeURIComponent(str).replace(/%20/g, '+');
return `mailto:${encode(to)}?subject=${encode(subject)}&body=${encode(body)}`;
}
export const item_checkout = async (item: IComponentConfig) => {
return `mailto:${DEFAULT_CONTACT}?subject=${item.name}&body=${""}`
}

View File

@ -1,63 +1,220 @@
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 { RenderedContent, DataEntry } from "astro:content"
import type { Loader, LoaderContext } from 'astro/loaders'
import { get } from '@polymech/commons/component'
import {
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
PRODUCT_ROOT, CAD_EXPORT_CONFIGURATIONS,
CAD_DEFAULT_CONFIGURATION,
CAD_URL,
parseBoolean,
DEFAULT_CONTACT,
default_image,
HOWTO_ROOT,
HOWTO_GLOB
} from 'config/config.js'
import { env } from '@/base/index.js'
import { gallery } from '@/base/media.js';
import { PFilterValid } from '@polymech/commons/filter'
import { IAssemblyData } from '@polymech/cad'
import { logger as log } from '@/base/index.js'
import { translate } from "@/base/i18n.js"
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
import { slugify } from "@/base/strings.js"
export const ITEM_TYPE = 'howto'
export const items = (branch: string) => get(`${HOWTO_ROOT()}/${HOWTO_GLOB}`, HOWTO_ROOT(), ITEM_TYPE)
export const defaults = async (data: any, cwd: string, root:string): Promise<IComponentConfigEx> => {
let defaultsJSON = await findUp('defaults.json', {
stopAt: root,
cwd: cwd
});
try {
if (defaultsJSON) {
data = {
...read(defaultsJSON, 'json') as any,
...data,
};
}
} catch (error) {
}
return data;
};
const onItem = async (item: any, ctx: LoaderContext) => {
return item
if (!item || !item.data) {
ctx.logger.error(`Error completing ${''}: no data`);
return
}
let data: any = item.data
const itemRel = data.rel
const itemRelMin = itemRel.replace('products/', '')
const itemDir = PRODUCT_DIR(itemRel)
const default_profile = env(itemRel)
data.product_rel = itemRelMin
data.assets = {
renderings: [],
gallery: []
}
data.body = (read(path.join(itemDir, 'templates/shared', 'body.md')) as string) || ""
data.resources = (read(path.join(itemDir, 'templates/shared', 'resources.md')) as string) || ""
//////////////////////////////////////////
//
// Item Shared Resources
//
let resourcesDefaultPath = await findUp('resources.md', {
stopAt: PRODUCT_ROOT(),
cwd: itemDir
}) || ""
data.shared = (read(resourcesDefaultPath) as string) || ""
//////////////////////////////////////////
//
// Readme
//
let readmePath = path.join(itemDir, 'Readme.md')
data.readme = (read(readmePath) as string) || ""
//////////////////////////////////////////
//
// Variables
//
data = await defaults(data, itemDir, PRODUCT_ROOT());
data = {
...data,
...default_profile.variables,
product_rel_min: itemRelMin,
}
// data = resolveConfig(data as Record<string, string>) as IComponentConfigEx
item.data = data
//////////////////////////////////////////
//
// Extensions, CAD, Media, etcetera etcetera :)
//
//data.cad = await cad(item)
data.assets.gallery = await gallery('media/gallery', data.rel)
}
export function loader(branch: string): Loader {
const load = async ({
config,
logger,
watcher,
parseData,
store,
generateDigest }: LoaderContext) => {
store.clear();
let products = items(branch)
for (const item of products) {
const product: any = item.config
const id = product.slug;
const data = {
rel: item.rel,
slug: id,
id,
title: product.name,
type: ITEM_TYPE,
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
} as any)
storeItem.data['config'] = JSON.stringify(storeItem.data, null, 2)
store.set(storeItem)
}
}
return {
name: `astro:store:${ITEM_TYPE}`,
load
};
}
export interface IHowto {
_createdBy: string
mentions: any[]
_deleted: boolean
fileLink: string
slug: string
_modified: string
previousSlugs: string[]
_created: string
description: string
votedUsefulBy: string[]
creatorCountry: string
total_downloads: number
title: string
time: string
files: any[]
difficulty_level: string
_id: string
tags?: Tags
total_views: number
_contentModifiedTimestamp: string
cover_image: CoverImage
comments: any[]
moderatorFeedback: string
steps: Step[]
moderation: string
}
_createdBy: string
mentions: any[]
_deleted: boolean
fileLink: string
slug: string
_modified: string
previousSlugs: string[]
_created: string
description: string
votedUsefulBy: string[]
creatorCountry: string
total_downloads: number
title: string
time: string
files: any[]
difficulty_level: string
_id: string
tags?: Tags
total_views: number
_contentModifiedTimestamp: string
cover_image: CoverImage
comments: any[]
moderatorFeedback: string
steps: Step[]
moderation: string
}
export interface Tags {
[key: string]: boolean
}
export interface CoverImage {
name: string
downloadUrl: string
type: string
fullPath: string
updated: string
size: number
timeCreated: string
contentType: string
}
export interface Step {
title: string
text: string
images: Image[]
_animationKey: string
}
export interface Image {
updated: string
size: number
fullPath: string
timeCreated: string
name: string
downloadUrl: string
contentType: string
type: string
}
export interface Tags {
[key: string]: boolean
}
export interface CoverImage {
name: string
downloadUrl: string
type: string
fullPath: string
updated: string
size: number
timeCreated: string
contentType: string
}
export interface Step {
title: string
text: string
images: Image[]
_animationKey: string
}
export interface Image {
updated: string
size: number
fullPath: string
timeCreated: string
name: string
downloadUrl: string
contentType: string
type: string
}

View File

@ -0,0 +1,30 @@
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ url }) => {
// Extract the full image URL from query parameters
const searchParams = new URL(url).searchParams;
const imageUrl = searchParams.get('url');
if (!imageUrl) {
return new Response('Missing image URL', { status: 400 });
}
try {
// Fetch the image from Firebase
const response = await fetch(imageUrl);
if (!response.ok) {
return new Response('Failed to fetch image', { status: response.status });
}
// Get the content type of the image
const contentType = response.headers.get('content-type') || 'image/jpeg';
// Stream the image response
return new Response(response.body, {
headers: { 'Content-Type': contentType },
});
} catch (error) {
return new Response('Error fetching image', { status: 500 });
}
};