diff --git a/package-lock.json b/package-lock.json index 9c48cf6..04ea610 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "hast-util-format": "^1.1.0", "hast-util-from-html": "^2.0.3", + "hast-util-heading": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-minify-whitespace": "^1.0.1", "hast-util-select": "^6.0.3", @@ -3339,6 +3340,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-heading": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading/-/hast-util-heading-3.0.0.tgz", + "integrity": "sha512-SykluYSLOs7z72hUUcztJpPV20alz58pfbi8g/NckXPnJ4OFVwPidNz3XOqgSNu5MTeFvde5c0cFVUk319Qlqw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-body-ok-link": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", diff --git a/package.json b/package.json index 706a3e8..9f1d425 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "hast-util-format": "^1.1.0", "hast-util-from-html": "^2.0.3", + "hast-util-heading": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-minify-whitespace": "^1.0.1", "hast-util-select": "^6.0.3", diff --git a/src/handlers/get.js b/src/handlers/get.js index 525166c..39a2ee2 100644 --- a/src/handlers/get.js +++ b/src/handlers/get.js @@ -13,6 +13,7 @@ import { get404, getRobots } from '../responses/index.js'; import { handleAEMProxyRequest } from '../routes/aem-proxy.js'; import { getCookie } from '../routes/cookie.js'; import { daSourceGet } from '../routes/da-admin.js'; +import { handleUEJsonRequest } from '../routes/ue-definitions.js'; export default async function getHandler({ req, env, daCtx }) { const { path } = daCtx; @@ -20,8 +21,8 @@ export default async function getHandler({ req, env, daCtx }) { if (!daCtx.site) return get404(); if (path.startsWith('/favicon.ico')) return get404(); if (path.startsWith('/robots.txt')) return getRobots(); - if (path.startsWith('/gimme_cookie')) return getCookie({ req }); + if (path.startsWith('/.da-ue')) return handleUEJsonRequest({ req, env, daCtx }); const resourceRegex = /\.(css|js|png|jpg|jpeg|webp|gif|svg|ico|json|xml|woff|woff2|plain\.html)$/i; if (resourceRegex.test(path)) { diff --git a/src/routes/da-admin.js b/src/routes/da-admin.js index d6efffe..3868514 100644 --- a/src/routes/da-admin.js +++ b/src/routes/da-admin.js @@ -106,12 +106,12 @@ export async function daSourceGet({ req, env, daCtx }) { if (daAdminResp && daAdminResp.status === 200) { // enrich stored content with HTML header and UE attributes const originalBodyHtml = await daAdminResp.text(); - const responseHtml = await prepareHtml(daCtx, aemCtx, originalBodyHtml, headHtml); + const responseHtml = await prepareHtml(env, daCtx, aemCtx, originalBodyHtml, headHtml); response.body = responseHtml; } else { // enrich default template with HTML header and UE attributes const templateHtml = await getPageTemplate(env, daCtx, aemCtx, headHtml); - const responseHtml = await prepareHtml(daCtx, aemCtx, templateHtml, headHtml); + const responseHtml = await prepareHtml(env, daCtx, aemCtx, templateHtml, headHtml); response.body = responseHtml; } diff --git a/src/routes/ue-definitions.js b/src/routes/ue-definitions.js new file mode 100644 index 0000000..1712544 --- /dev/null +++ b/src/routes/ue-definitions.js @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { UE_JSON_FILES } from '../utils/constants.js'; +import { get404 } from '../responses/index.js'; +import { fetchBlockLibrary, getComponentDefinitions, getComponentFilters, getComponentModels } from '../ue/definitions.js'; + +/** + * Main handler for the DA block library route + */ +export async function handleUEJsonRequest({ req, env, daCtx }) { + // request routing, we only support component-definition.json, component-models.json + // and component-filters.json + + // TODO get JSON from KW store, if not found render from block library + const path = new URL(req.url).pathname; + const jsonPath = path.replace('/.da-ue/', ''); + if (!UE_JSON_FILES.includes(jsonPath)) { + return get404(); + } + + const blocks = await fetchBlockLibrary(env, daCtx); + + if (path === '/.da-ue/component-definition.json') { + const componentDefinitions = getComponentDefinitions(blocks); + return new Response( + JSON.stringify(componentDefinitions, null, 2), + { + headers: { 'Content-Type': 'application/json' }, + }, + ); + } else if (path === '/.da-ue/component-models.json') { + const componentModels = getComponentModels(blocks); + return new Response(JSON.stringify(componentModels, null, 2), { + headers: { 'Content-Type': 'application/json' }, + }); + } else if (path === '/.da-ue/component-filters.json') { + const componentFilters = getComponentFilters(blocks); + return new Response(JSON.stringify(componentFilters, null, 2), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + return get404(); +} diff --git a/src/ue/definitions.js b/src/ue/definitions.js new file mode 100644 index 0000000..a8af3c1 --- /dev/null +++ b/src/ue/definitions.js @@ -0,0 +1,365 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { fromHtml } from 'hast-util-from-html'; +import { select, selectAll } from 'hast-util-select'; +import { heading } from 'hast-util-heading'; +import { toString } from 'hast-util-to-string'; +import { isElement } from 'hast-util-is-element'; +import { toHtml } from 'hast-util-to-html'; +import { getFirstSheet } from '../utils/sheet.js'; +import { getBlockNameAndClasses } from '../utils/hast.js'; +import { toClassName } from '../utils/strings.js'; +import { AEM_ORIGINS, DEFAULT_COMPONENT_DEFINITIONS, DEFAULT_COMPONENT_FILTERS, DEFAULT_COMPONENT_MODELS } from '../utils/constants.js'; + +// URL for the DA block collection +// const BLOCK_LIBRARY_URL = +// 'https://content.da.live/aemsites/vitamix/docs/library/blocks.json'; +// +// const BLOCK_LIBRARY_URL = 'https://content.da.live/aemsites/da-block-collection/docs/library/blocks.json'; +const BLOCK_LIBRARY_URL = 'https://content.da.live/mhaack/special-project/docs/library/blocks.json'; + +async function getBlocks(env, daCtx, sources) { + try { + const sourcesData = await Promise.all( + sources.map(async (url) => { + try { + const resp = await fetch(url, { + headers: { + Authorization: daCtx.authToken, + }, + }); + if (!resp.ok) throw new Error('Something went wrong.'); + return resp.json(); + } catch { + return null; + } + }), + ); + + const blockList = []; + sourcesData.forEach((blockData) => { + if (!blockData) return; + const data = getFirstSheet(blockData); + if (!data) return; + data.forEach((block) => { + if (block.name && block.path) blockList.push(block); + }); + }); + + return blockList; + } catch (error) { + console.error('Error fetching blocks:', error); + return []; + } +} + +/** + * Extracts key-value pairs from a block element. + * Each pair is expected to be in a div structure where the first child is the key + * and the second child is the value. + * + * @param {Element} element - The block element containing key-value pairs + * @returns {Array<{key: string, value: string}>} Array of key-value objects + */ +function getKeyValuesFromBlock(element) { + const keyValues = []; + const keyValuePairs = selectAll(':scope > div', element); + keyValuePairs.forEach((pair) => { + const key = toClassName(toString(pair.children[0])); + const value = toString(pair.children[1]); + keyValues.push({ key, value }); + }); + return keyValues; +} + +async function getBlockVariants(env, daCtx, path) { + const { origin } = new URL(path); + const isAemHosted = AEM_ORIGINS.some((aemOrigin) => origin.endsWith(aemOrigin)); + + const postfix = isAemHosted ? '.plain.html' : ''; + const resp = await fetch(`${path}${postfix}`); // TODO fetch via da-content and with token from daCtx + if (!resp.ok) return []; + + const html = await resp.text(); + if (!html) return []; + + const hast = fromHtml(html, { fragment: true }); + const elements = selectAll('main > div > div, main > div > h1, main > div > h2, main > div > h3, main > div > h4, main > div > h5, main > div > h6', hast); + + const variants = []; + elements.forEach((element, index) => { + if (isElement(element, 'div')) { + const blockConfig = getBlockNameAndClasses(element); + if (blockConfig.name !== 'library-metadata') { + const block = { + model: blockConfig.name, + classes: blockConfig.classes.filter((cls) => cls !== blockConfig.name), + name: blockConfig.name, + hast: element, + }; + + if (elements[index - 1] + && heading(elements[index - 1]) + && elements[index - 1].children.length > 0) { + block.name = toString(elements[index - 1]); + } + + if (elements[index + 1] && isElement(elements[index + 1], 'div')) { + const libraryBlockConfig = getBlockNameAndClasses(elements[index + 1]); + if (libraryBlockConfig.name === 'library-metadata') { + const libraryMetadata = elements[index + 1]; + const keyValues = getKeyValuesFromBlock(libraryMetadata); + keyValues.forEach((keyValue) => { + block[keyValue.key] = keyValue.value; + }); + } + } + + block.id = toClassName(block.name); + variants.push(block); + } + } + }); + return variants; +} + +function getUniqueBlockVariants(block) { + const blockId = toClassName(block.name); + + let uniqueBlockVariants = block.variants.reduce((acc, variant) => { + const isDuplicate = acc.some( + (v) => v.id === variant.id && v.name === variant.name, + ); + if (!isDuplicate) { + acc.push(variant); + } + return acc; + }, []); + + // if there are multiple variants, we need to add the core block definition and + // the variants with individual sample content and unique id + // only the "core" block definition get the model from the block name + // otherise add the variants with the model from the block name + if (uniqueBlockVariants.length > 1) { + const baseBlockVariant = { + name: block.name, + id: blockId, + model: toClassName(block.name), + }; + uniqueBlockVariants = [baseBlockVariant, ...uniqueBlockVariants]; + } + + return uniqueBlockVariants; +} + +export async function fetchBlockLibrary(env, daCtx) { + const blockList = await getBlocks(env, daCtx, [BLOCK_LIBRARY_URL]); + + const blocks = await Promise.all( + (blockList || []).map(async (block) => { + try { + const blockVariants = await getBlockVariants(env, daCtx, block.path); + if (blockVariants.length === 0) return { error: 'no blocks found' }; + return { + name: block.name, + path: block.path, + group: block.group || 'blocks', + items: block.items || null, + variants: blockVariants, + }; + } catch (e) { + console.error('Error fetching block details:', e); + return { error: 'no blocks found' }; + } + }), + ); + + // Filter out blocks with errors and flatten the array + return blocks.filter((block) => !block.error).flat(); +} + +export function getComponentDefinitions(blocks) { + const definitions = structuredClone(DEFAULT_COMPONENT_DEFINITIONS); + const defaultBlocksDefinition = definitions.groups.find((definition) => definition.id === 'blocks'); + + blocks + .filter((block) => !block.name.toLowerCase().includes('metadata')) + .forEach((block) => { + let blockGroup = defaultBlocksDefinition; + if (block.group !== 'blocks') { + blockGroup = { + title: block.name, + id: toClassName(block.group), + components: [], + }; + definitions.groups.push(blockGroup); + } + + const blockId = toClassName(block.name); + const uniqueBlockVariants = getUniqueBlockVariants(block); + + // map block variants to component definitions + uniqueBlockVariants.forEach((variant, index) => { + const blockVariant = { + title: variant.name, + id: index === 0 ? variant.id : `${variant.id}-${index}`, + model: variant.model || null, + }; + if (variant.hast) { + blockVariant.plugins = { + da: { + unsafeHTML: toHtml(variant.hast), + }, + }; + } + blockGroup.components.push(blockVariant); + + // for container blocks, get the first and add the item block with the sample content + if (block.items && index === 0) { + const item = { + title: block.items, + id: `${blockId}-item`, + }; + + const firstVariant = uniqueBlockVariants.length > 1 + ? uniqueBlockVariants[1] + : uniqueBlockVariants[0]; + if (firstVariant.hast) { + const itemHast = select('div > div', firstVariant.hast); + item.plugins = { + da: { + unsafeHTML: toHtml(itemHast), + }, + }; + } + blockGroup.components.push(item); + } + }); + }); + + return definitions; +} + +export function getComponentModels(blocks) { + const models = structuredClone(DEFAULT_COMPONENT_MODELS); + + blocks.filter((block) => !block.name.toLowerCase().includes('metadata')).forEach((block) => { + const blockId = toClassName(block.name); + const model = { + id: blockId, + fields: [], + }; + // for each block which has different classes we need to add a model + const variantClasses = block.variants + .filter((variant) => variant.classes) + .flatMap((variant) => variant.classes); + if (variantClasses.length > 0) { + const field = { + component: 'multiselect', + name: 'classes', + label: 'Styles', + options: variantClasses.map((cls) => ({ + name: cls, + value: cls, + })), + }; + + model.fields.push(field); + } + + // add model fields based on block structure + const uniqueBlockVariants = getUniqueBlockVariants(block); + const blockVariant = uniqueBlockVariants.length > 1 + ? uniqueBlockVariants[1] + : uniqueBlockVariants[0]; + if (blockVariant.hast) { + const cells = select(':scope > div', blockVariant.hast).children; + + const createFields = (cell, index) => { + if (select('picture', cell) && cell.children.length < 2) { + return [ + { + component: 'reference', + name: `div:nth-child(${ + index + 1 + })>picture:nth-child(1)>img:nth-child(3)[src]`, + label: 'Image', + }, + { + component: 'text', + name: `div:nth-child(${ + index + 1 + })>picture:nth-child(1)>img:nth-child(3)[alt]`, + label: 'Image Alt Text', + }, + ]; + } + return [ + { + component: 'richtext', + name: `div:nth-child(${index + 1})`, + label: 'Text', + }, + ]; + }; + + if (!block.items) { + // Simple blocks + cells.forEach((cell, index) => { + model.fields.push(...createFields(cell, index)); + }); + } else { + // Container blocks + const itemModel = { + id: `${blockId}-item`, + fields: cells.flatMap((cell, index) => createFields(cell, index)).flat(), + }; + models.push(itemModel); + } + } + + if (model.fields.length > 0) { + models.push(model); + } + }); + + return models; +} + +export function getComponentFilters(blocks) { + const filters = structuredClone(DEFAULT_COMPONENT_FILTERS); + const sectionFilter = filters.find((filter) => filter.id === 'section'); + if (sectionFilter) { + blocks.filter((block) => !block.name.toLowerCase().includes('metadata')) + .forEach((block) => { + const blockId = toClassName(block.name); + const uniqueBlockVariants = getUniqueBlockVariants(block); + uniqueBlockVariants.forEach((variant, index) => { + const id = index === 0 ? variant.id : `${variant.id}-${index}`; + sectionFilter.components.push(id); + }); + + if (block.items) { + const filter = { + id: blockId, + components: [ + `${blockId}-item`, + ], + }; + filters.push(filter); + } + }); + } + + return filters; +} diff --git a/src/ue/metadata.js b/src/ue/metadata.js index 8945b32..650407f 100644 --- a/src/ue/metadata.js +++ b/src/ue/metadata.js @@ -12,6 +12,7 @@ import { select } from 'hast-util-select'; import { readBlockConfig } from '../utils/hast.js'; +import { toMetaName } from '../utils/strings.js'; export function extractLocalMetadata(bodyTree) { const metaBlock = select('div.metadata', bodyTree); @@ -23,15 +24,6 @@ export function extractLocalMetadata(bodyTree) { return metaConfig; } -/** - * Converts all non-valid characters to `-`. - * @param {string} text input text - * @returns {string} the meta name - */ -export function toMetaName(text) { - return text.toLowerCase().replace(/[^0-9a-z:_]/gi, '-'); -} - export function globToRegExp(glob) { const reString = glob .replaceAll('**', '|') diff --git a/src/ue/scaffold.js b/src/ue/scaffold.js index fbf0e3b..e16ec2c 100644 --- a/src/ue/scaffold.js +++ b/src/ue/scaffold.js @@ -12,6 +12,7 @@ import { fromHtml } from 'hast-util-from-html'; import { h } from 'hastscript'; +import { fetchBlockLibrary, getComponentDefinitions, getComponentFilters, getComponentModels } from './definitions'; export function getHtmlDoc() { const htmlDocStr = ''; @@ -55,7 +56,7 @@ export function getUEHtmlHeadEntries(daCtx, aemCtx) { type: 'application/vnd.adobe.aue.component+json', src: orgSiteInPath ? `/${org}/${site}/component-definition.json` - : '/component-definition.json', + : '/.da-ue/component-definition.json', }), ); children.push( @@ -63,7 +64,7 @@ export function getUEHtmlHeadEntries(daCtx, aemCtx) { type: 'application/vnd.adobe.aue.model+json', src: orgSiteInPath ? `/${org}/${site}/component-models.json` - : '/component-models.json', + : '/.da-ue/component-models.json', }), ); children.push( @@ -71,7 +72,7 @@ export function getUEHtmlHeadEntries(daCtx, aemCtx) { type: 'application/vnd.adobe.aue.filter+json', src: orgSiteInPath ? `/${org}/${site}/component-filters.json` - : '/component-filters.json', + : '/.da-ue/component-filters.json', }), ); @@ -128,3 +129,18 @@ export async function getUEConfig(aemCtx) { return ueConfig; } + +// TODO make this universal for either AEM or DA internal +export async function getUEConfig2(env, daCtx) { + const blocks = await fetchBlockLibrary(env, daCtx); + const componentDefinitions = getComponentDefinitions(blocks); + const componentModels = getComponentModels(blocks); + const componentFilters = getComponentFilters(blocks); + + const ueConfig = { + 'component-model': componentModels, + 'component-definition': componentDefinitions, + 'component-filter': componentFilters, + }; + return ueConfig; +} diff --git a/src/ue/ue.js b/src/ue/ue.js index 7d27807..79c9d4b 100644 --- a/src/ue/ue.js +++ b/src/ue/ue.js @@ -15,7 +15,7 @@ import { fromHtml } from 'hast-util-from-html'; import { select, selectAll } from 'hast-util-select'; import { toHtml } from 'hast-util-to-html'; import { h } from 'hastscript'; -import { getHtmlDoc, getUEConfig, getUEHtmlHeadEntries } from './scaffold.js'; +import { getHtmlDoc, getUEConfig2, getUEHtmlHeadEntries } from './scaffold.js'; import { injectUEAttributes } from './attributes.js'; import { extractLocalMetadata, fetchBulkMetadata } from './metadata.js'; import rewriteIcons from './rewrite-icons.js'; @@ -73,7 +73,7 @@ function injectMetadata(metadata, headNode) { }); } -export async function prepareHtml(daCtx, aemCtx, bodyHtmlStr, headHtmlStr) { +export async function prepareHtml(env, daCtx, aemCtx, bodyHtmlStr, headHtmlStr) { // get the HTML document tree const documentTree = getHtmlDoc(); @@ -104,7 +104,7 @@ export async function prepareHtml(daCtx, aemCtx, bodyHtmlStr, headHtmlStr) { rewriteIcons(bodyNode); // add data attributes for UE to the body - const ueConfig = await getUEConfig(aemCtx); + const ueConfig = await getUEConfig2(env, daCtx); injectUEAttributes(bodyNode, ueConfig); // output the final HTML document diff --git a/src/utils/constants.js b/src/utils/constants.js index 2da46a8..035f102 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -12,6 +12,14 @@ export const TRUSTED_ORIGINS = ['https://experience.adobe.com', 'https://localhost.corp.adobe.com:8080']; +export const AEM_ORIGINS = ['hlx.page', 'hlx.live', 'aem.page', 'aem.live']; + +export const UE_JSON_FILES = [ + 'component-definition.json', + 'component-models.json', + 'component-filters.json', +]; + export const DEFAULT_CORS_HEADERS = { 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', @@ -31,3 +39,134 @@ export const UNAUTHORIZED_HTML_MESSAGE = ` export const DEFAULT_HTML_TEMPLATE = '
'; export const BRANCH_NOT_FOUND_HTML_MESSAGE = '

Not found: Unable to retrieve AEM branch

'; + +export const DEFAULT_COMPONENT_DEFINITIONS = { + groups: [ + { + title: 'Default Content', + id: 'default', + components: [ + { + title: 'Text', + id: 'text', + plugins: { + da: { + name: 'text', + type: 'text', + }, + }, + }, + { + title: 'Image', + id: 'image', + plugins: { + da: { + name: 'image', + type: 'image', + }, + }, + }, + ], + }, + { + title: 'Sections', + id: 'sections', + components: [ + { + title: 'Section', + id: 'section', + plugins: { + da: { + unsafeHTML: '
', + }, + }, + filter: 'section', + model: 'section', + }, + ], + }, + { + title: 'Blocks', + id: 'blocks', + components: [], + }, + ], +}; +export const DEFAULT_COMPONENT_FILTERS = [ + { + id: 'main', + components: [ + 'section', + ], + }, + { + id: 'section', + components: [ + 'text', + 'image', + ], + }, +]; +export const DEFAULT_COMPONENT_MODELS = [ + { + id: 'page-metadata', + fields: [ + { + component: 'container', + label: 'Fieldset', + fields: [ + { + component: 'text', + name: 'title', + label: 'Title', + }, + { + component: 'text', + name: 'description', + label: 'Description', + }, + { + component: 'text', + valueType: 'string', + name: 'robots', + label: 'Robots', + description: 'Index control via robots', + }, + ], + }, + ], + }, + { + id: 'image', + fields: [ + { + component: 'reference', + name: 'image', + hidden: true, + multi: false, + }, + { + component: 'reference', + name: 'img:nth-child(3)[src]', + label: 'Image', + multi: false, + }, + { + component: 'text', + name: 'img:nth-child(3)[alt]', + label: 'Alt Text', + }, + ], + }, + { + id: 'section', + fields: [ + { + component: 'text', + name: 'style', + label: 'Style', + }, + ], + }, +]; + diff --git a/src/utils/hast.js b/src/utils/hast.js index a5e4a19..0b933ad 100644 --- a/src/utils/hast.js +++ b/src/utils/hast.js @@ -12,21 +12,12 @@ import { selectAll, select } from 'hast-util-select'; import { toString } from 'hast-util-to-string'; import { visit } from 'unist-util-visit'; +import { toMetaName } from './strings.js'; export function childNodes(node) { return node.children.filter((n) => n.type === 'element'); } -/** - * Converts all non-valid characters to `-`. - * @param {string} text input text - * @returns {string} the meta name - */ -export function toMetaName(text) { - return text - .toLowerCase() - .replace(/[^0-9a-z:_]/gi, '-'); -} /** * Returns the config from a block element as object with key/value pairs. * @param {Element} $block The block element diff --git a/src/utils/sheet.js b/src/utils/sheet.js new file mode 100644 index 0000000..7778cc0 --- /dev/null +++ b/src/utils/sheet.js @@ -0,0 +1,20 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export const getSheetByIndex = (json, index = 0) => { + if (json[':type'] !== 'multi-sheet') { + return json.data; + } + return json[Object.keys(json)[index]]?.data; +}; + +export const getFirstSheet = (json) => getSheetByIndex(json, 0); diff --git a/src/utils/strings.js b/src/utils/strings.js new file mode 100644 index 0000000..bec3ea1 --- /dev/null +++ b/src/utils/strings.js @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * Sanitizes a string for use as class name. + * @param {string} name The unsanitized string + * @returns {string} The class name + */ +export function toClassName(name) { + return typeof name === 'string' + ? name + .toLowerCase() + .replace(/[^0-9a-z]/gi, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + : ''; +} + +/** + * Sanitizes a string for use as a js property name. + * @param {string} name The unsanitized string + * @returns {string} The camelCased name + */ +export function toCamelCase(name) { + return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +} + +/** + * Converts all non-valid characters to `-`. + * @param {string} text input text + * @returns {string} the meta name + */ +export function toMetaName(text) { + return text + .toLowerCase() + .replace(/[^0-9a-z:_]/gi, '-'); +}