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`).
This commit is contained in:
314
packages/frontend/templates/build-edgeless.mjs
Normal file
314
packages/frontend/templates/build-edgeless.mjs
Normal file
@@ -0,0 +1,314 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user