AFFiNE/packages/frontend/templates/build-edgeless.mjs
Lye Hongtao 4641bc422b
feat: move templates into AFFiNE (#5750)
Related to https://github.com/toeverything/blocksuite/pull/6156

### Change
Move the edgeless templates to AFFiNE. All templates are stored as zip files. Run `node build-edgeless.mjs` in `@affine/templates` to generate JSON-format templates and importing script. The template will be generated automatically during building and dev (`yarn dev`).
2024-02-21 06:26:01 +00:00

315 lines
7.8 KiB
JavaScript

import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
import fs from 'node:fs/promises';
import path, { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import JSZip from 'jszip';
const __dirname = join(fileURLToPath(import.meta.url), '..');
const ZIP_PATH = join(__dirname, './edgeless-snapshot');
const ASSETS_PREFIX = `/static/templates`;
const ASSETS_PATH = join(__dirname, '../core/public/', ASSETS_PREFIX);
const TEMPLATE_PATH = join(__dirname, './edgeless');
const getZipFilesInCategroies = () => {
return fs.readdir(ZIP_PATH).then(folders => {
return Promise.all(
folders
.filter(folder => {
return statSync(join(ZIP_PATH, folder)).isDirectory();
})
.map(async folder => {
const files = await fs.readdir(join(ZIP_PATH, folder));
return {
category: folder,
files: files.filter(file => path.extname(file) === '.zip'),
};
})
);
});
};
const setupFolder = async () => {
if (!existsSync(ASSETS_PATH)) {
mkdirSync(ASSETS_PATH);
}
if (!existsSync(TEMPLATE_PATH)) {
mkdirSync(TEMPLATE_PATH);
}
};
/**
* @typedef Block
* @type {object}
* @property {string} flavour
* @property {Array<Block> | undefined} children
* @property {object} props
* @property {string} props.sourceId
*/
/**
* @param {Block} block
*/
const convertSourceId = (block, assetsExtMap) => {
if (block.props?.sourceId) {
const extname = assetsExtMap[block.props.sourceId];
if (!extname) {
console.warn(`No extname found for ${block.props.sourceId}`);
}
block.props.sourceId = `${ASSETS_PREFIX}/${block.props.sourceId}${
extname ?? ''
}`;
}
if (block.children && Array.isArray(block.children)) {
block.children.forEach(block => convertSourceId(block, assetsExtMap));
}
};
const parseSnapshot = async () => {
const filesInCategroies = await getZipFilesInCategroies();
await setupFolder();
/**
* @type {Array<{ category: string, templates: string[] }}>}
*/
const templatesInCategory = [];
for (let cate of filesInCategroies) {
const templates = [];
const assetsExtentionMap = {};
for (let file of cate.files) {
const templateName = path.basename(file, '.zip');
const zip = new JSZip();
const { files: unarchivedFiles } = await zip.loadAsync(
readFileSync(join(ZIP_PATH, cate.category, file))
);
/**
* @type {Array<JSZip.JSZipObject>}
*/
const assetsFiles = [];
/**
* @type {Array<JSZip.JSZipObject>}
*/
const snapshotFiles = [];
Object.entries(unarchivedFiles).forEach(([name, fileObj]) => {
if (name.includes('MACOSX') || name.includes('__MACOSX')) return;
if (name.startsWith('assets/') && !fileObj.dir) {
assetsFiles.push(fileObj);
return;
}
if (name.endsWith('.snapshot.json')) {
snapshotFiles.push(fileObj);
return;
}
});
await Promise.all(
assetsFiles.map(async file => {
const blob = await file.async('blob');
const arrayBuffer = await blob.arrayBuffer();
const buffer = Buffer.from(arrayBuffer, 'binary');
const extname = path.extname(file.name);
assetsExtentionMap[
file.name.replace('assets/', '').replace(extname, '')
] = extname;
await fs.writeFile(
join(ASSETS_PATH, file.name.replace('assets/', '')),
buffer
);
})
);
await Promise.all(
snapshotFiles.map(async snapshot => {
const json = await snapshot.async('text');
const snapshotContent = JSON.parse(json);
const previewPath = join(
ZIP_PATH,
cate.category,
`${templateName}.svg`
);
let previewContent = '';
if (existsSync(previewPath)) {
const previewFile = readFileSync(previewPath, 'utf-8');
previewContent = previewFile
.replace(/\n/g, '')
.replace(/\s+/g, ' ')
.replace('fill="white"', 'fill="currentColor"');
} else {
console.warn(`No preview found for ${templateName}`);
}
convertSourceId(snapshotContent.blocks, assetsExtentionMap);
const template = {
name: templateName,
type: 'template',
preview: previewContent,
content: snapshotContent,
};
await fs.writeFile(
join(join(TEMPLATE_PATH, `${templateName}.json`)),
JSON.stringify(template, undefined, 2)
);
templates.push(templateName);
})
);
}
templatesInCategory.push({
category: cate.category,
templates,
});
}
return templatesInCategory;
};
function numberToWords(n) {
const ones = [
'Zero',
'One',
'Two',
'Three',
'Four',
'Five',
'Six',
'Seven',
'Eight',
'Nine',
];
if (n < 10) {
return ones[n];
} else {
throw new Error(`Not implemented: ${n}`);
}
}
const camelCaseNumber = variable => {
const words = variable.split(' ');
return words
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
};
const toVariableName = name => {
const converted = Array.from(name).reduce((pre, char) => {
if (char >= '0' && char <= '9') {
return pre + numberToWords(char - '0');
}
return pre + char;
}, '');
return camelCaseNumber(converted);
};
/**
*
* @param {Array<{category: string, templates: string[]}} templatesInGroup
*/
const buildScript = async templatesInGroup => {
const templates = [];
const templateVariableMap = {};
templatesInGroup.forEach(group => {
group.templates.forEach(template => {
templates.push(template);
templateVariableMap[template] = toVariableName(template);
});
});
const importStatements = templates
.map(template => {
return `import ${toVariableName(
template
)} from './edgeless/${template}.json';`;
})
.join('\n');
const templatesDeclaration = templatesInGroup.map(group => {
return `'${group.category}': [
${group.templates
.map(template => templateVariableMap[template])
.join(',\n ')}
]`;
});
const code = `${importStatements}
const templates = {
${templatesDeclaration.join(',\n ')}
}
function lcs(text1: string, text2: string) {
const dp: number[][] = Array.from({ length: text1.length + 1 })
.fill(null)
.map(() => Array.from<number>({length: text2.length + 1}).fill(0));
for (let i = 1; i <= text1.length; i++) {
for (let j = 1; j <= text2.length; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.length][text2.length];
}
export const builtInTemplates = {
list: async (category: string) => {
// @ts-expect-error type should be asserted when using
return templates[category] ?? []
},
categories: async () => {
return Object.keys(templates)
},
search: async(query: string) => {
const candidates: unknown[] = [];
const cates = Object.keys(templates);
query = query.toLowerCase();
for(let cate of cates) {
// @ts-expect-error type should be asserted when using
const templatesOfCate = templates[cate];
for(let temp of templatesOfCate) {
if(lcs(query, temp.name.toLowerCase()) === query.length) {
candidates.push(temp);
}
}
}
return candidates;
},
}
`;
await fs.writeFile(join(__dirname, './edgeless-templates.gen.ts'), code, {
encoding: 'utf-8',
});
};
async function main() {
const templatesInGroup = await parseSnapshot();
await buildScript(templatesInGroup);
}
main();