-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: move templates into AFFiNE (#5750)
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
1 parent
75d5867
commit e049113
Showing
42 changed files
with
896 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
Oops, something went wrong.