Skip to content

Commit

Permalink
feat: move templates into AFFiNE (#5750)
Browse files Browse the repository at this point in the history
Related to toeverything/blocksuite#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`).
  • Loading branch information
doouding authored and Brooooooklyn committed Feb 21, 2024
1 parent 75d5867 commit e049113
Show file tree
Hide file tree
Showing 42 changed files with 896 additions and 2 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ static
web-static
public
packages/frontend/i18n/src/i18n-generated.ts
packages/frontend/templates/edgeless-templates.gen.ts
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ lib
affine.db
apps/web/next-routes.conf
.nx

packages/frontend/templates/edgeless
packages/frontend/core/public/static/templates
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ packages/frontend/i18n/src/i18n-generated.ts
packages/frontend/graphql/src/graphql/index.ts
tests/affine-legacy/**/static
.yarnrc.yml
packages/frontend/templates/edgeless-templates.gen.ts
packages/frontend/templates/templates.gen.ts
packages/frontend/templates/onboarding

Expand Down
7 changes: 7 additions & 0 deletions packages/frontend/core/src/bootstrap/edgeless-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { builtInTemplates } from '@affine/templates/edgeless';
import {
EdgelessTemplatePanel,
type TemplateManager,
} from '@blocksuite/blocks';

EdgelessTemplatePanel.templates.extend(builtInTemplates as TemplateManager);
1 change: 1 addition & 0 deletions packages/frontend/core/src/bootstrap/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './register-blocksuite-components';
import './edgeless-template';

import { setupGlobal } from '@affine/env/global';
import * as Sentry from '@sentry/react';
Expand Down
314 changes: 314 additions & 0 deletions packages/frontend/templates/build-edgeless.mjs
Original file line number Diff line number Diff line change
@@ -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();
Loading

0 comments on commit e049113

Please sign in to comment.