From db5d81677f335ceb6c2432eb989668029d02e2e3 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Sun, 1 Sep 2024 11:40:50 +0800 Subject: [PATCH] feat(plugin-md-power): add support `file-tree` container --- .../src/client/components/FileTreeItem.vue | 143 ++++ .../src/node/features/fileTree/findIcon.ts | 74 ++ .../src/node/features/fileTree/icons.ts | 775 ++++++++++++++++++ .../src/node/features/fileTree/index.ts | 77 ++ .../features/fileTree/resolveTreeNodeInfo.ts | 125 +++ plugins/plugin-md-power/src/node/plugin.ts | 8 +- .../src/node/prepareConfigFile.ts | 6 + plugins/plugin-md-power/src/shared/plugin.ts | 1 + plugins/plugin-md-power/tsup.config.ts | 1 + theme/src/node/plugins/getPlugins.ts | 3 + 10 files changed, 1212 insertions(+), 1 deletion(-) create mode 100644 plugins/plugin-md-power/src/client/components/FileTreeItem.vue create mode 100644 plugins/plugin-md-power/src/node/features/fileTree/findIcon.ts create mode 100644 plugins/plugin-md-power/src/node/features/fileTree/icons.ts create mode 100644 plugins/plugin-md-power/src/node/features/fileTree/index.ts create mode 100644 plugins/plugin-md-power/src/node/features/fileTree/resolveTreeNodeInfo.ts diff --git a/plugins/plugin-md-power/src/client/components/FileTreeItem.vue b/plugins/plugin-md-power/src/client/components/FileTreeItem.vue new file mode 100644 index 00000000..78f9d263 --- /dev/null +++ b/plugins/plugin-md-power/src/client/components/FileTreeItem.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/plugins/plugin-md-power/src/node/features/fileTree/findIcon.ts b/plugins/plugin-md-power/src/node/features/fileTree/findIcon.ts new file mode 100644 index 00000000..20d56664 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/fileTree/findIcon.ts @@ -0,0 +1,74 @@ +import { FileIcons, definitions } from './icons.js' + +export interface FileIcon { + name: string + svg: string +} + +export const defaultFileIcon: FileIcon = { + name: 'default', + svg: makeSVGIcon(FileIcons['seti:default']), +} + +export const folderIcon: FileIcon = { + name: 'folder', + svg: makeSVGIcon(FileIcons['seti:folder']), +} + +export function getFileIcon(fileName: string): FileIcon { + const name = getFileIconName(fileName) + if (!name) + return defaultFileIcon + + if (name in FileIcons) { + const path = FileIcons[name as keyof typeof FileIcons] + return { + name: name.includes(':') ? name.split(':')[1] : name, + svg: makeSVGIcon(path), + } + } + return defaultFileIcon +} + +function makeSVGIcon(svg: string): string { + svg = `${svg}` + .replace(/"/g, '\'') + .replace(/%/g, '%25') + .replace(/#/g, '%23') + .replace(/\{/g, '%7B') + .replace(/\}/g, '%7D') + .replace(//g, '%3E') + return `url("data:image/svg+xml,${svg}")` +} + +function getFileIconName(fileName: string) { + let icon: string | undefined = definitions.files[fileName] + if (icon) + return icon + icon = getFileIconTypeFromExtension(fileName) + if (icon) + return icon + for (const [partial, partialIcon] of Object.entries(definitions.partials)) { + if (fileName.includes(partial)) + return partialIcon + } + return icon +} + +function getFileIconTypeFromExtension(fileName: string): string | undefined { + const firstDotIndex = fileName.indexOf('.') + if (firstDotIndex === -1) + return + let extension = fileName.slice(firstDotIndex) + while (extension !== '') { + const icon = definitions.extensions[extension] + if (icon) + return icon + const nextDotIndex = extension.indexOf('.', 1) + if (nextDotIndex === -1) + return + extension = extension.slice(nextDotIndex) + } + return undefined +} diff --git a/plugins/plugin-md-power/src/node/features/fileTree/icons.ts b/plugins/plugin-md-power/src/node/features/fileTree/icons.ts new file mode 100644 index 00000000..20db18d8 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/fileTree/icons.ts @@ -0,0 +1,775 @@ +/** + * Based on https://github.com/elviswolcott/seti-icons which + * is derived from https://github.com/jesseweed/seti-ui/ + * + * Copyright (c) 2014 Jesse Weed + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export interface Definitions { + files: Record + extensions: Record + partials: Record +} + +export const definitions: Definitions = { + files: { + 'pnpm-debug.log': 'pnpm', + 'pnpm-lock.yaml': 'pnpm', + 'pnpm-workspace.yaml': 'pnpm', + 'biome.json': 'biome', + 'bun.lockb': 'bun', + 'COMMIT_EDITMSG': 'seti:git', + 'MERGE_MSG': 'seti:git', + 'karma.conf.js': 'seti:karma', + 'karma.conf.cjs': 'seti:karma', + 'karma.conf.mjs': 'seti:karma', + 'karma.conf.coffee': 'seti:karma', + 'README.md': 'seti:info', + 'README.txt': 'seti:info', + 'README': 'seti:info', + 'CHANGELOG.md': 'seti:clock', + 'CHANGELOG.txt': 'seti:clock', + 'CHANGELOG': 'seti:clock', + 'CHANGES.md': 'seti:clock', + 'CHANGES.txt': 'seti:clock', + 'CHANGES': 'seti:clock', + 'VERSION.md': 'seti:clock', + 'VERSION.txt': 'seti:clock', + 'VERSION': 'seti:clock', + 'mvnw': 'seti:maven', + 'pom.xml': 'seti:maven', + 'tsconfig.json': 'seti:tsconfig', + 'swagger.json': 'seti:json', + 'swagger.yml': 'seti:json', + 'swagger.yaml': 'seti:json', + 'mime.types': 'seti:config', + 'Jenkinsfile': 'seti:jenkins', + 'babel.config.js': 'seti:babel', + 'babel.config.json': 'seti:babel', + 'babel.config.cjs': 'seti:babel', + 'BUILD': 'seti:bazel', + 'BUILD.bazel': 'seti:bazel', + 'WORKSPACE': 'seti:bazel', + 'WORKSPACE.bazel': 'seti:bazel', + 'bower.json': 'seti:bower', + 'Bower.json': 'seti:bower', + 'eslint.config.js': 'seti:eslint', + 'firebase.json': 'seti:firebase', + 'geckodriver': 'seti:firefox', + 'Gruntfile.js': 'seti:grunt', + 'gruntfile.babel.js': 'seti:grunt', + 'Gruntfile.babel.js': 'seti:grunt', + 'gruntfile.js': 'seti:grunt', + 'Gruntfile.coffee': 'seti:grunt', + 'gruntfile.coffee': 'seti:grunt', + 'ionic.config.json': 'seti:ionic', + 'Ionic.config.json': 'seti:ionic', + 'ionic.project': 'seti:ionic', + 'Ionic.project': 'seti:ionic', + 'platformio.ini': 'seti:platformio', + 'rollup.config.js': 'seti:rollup', + 'sass-lint.yml': 'seti:sass', + 'stylelint.config.js': 'seti:stylelint', + 'stylelint.config.cjs': 'seti:stylelint', + 'stylelint.config.mjs': 'seti:stylelint', + 'yarn.clean': 'seti:yarn', + 'yarn.lock': 'seti:yarn', + 'webpack.config.js': 'seti:webpack', + 'webpack.config.cjs': 'seti:webpack', + 'webpack.config.mjs': 'seti:webpack', + 'webpack.config.ts': 'seti:webpack', + 'webpack.config.build.js': 'seti:webpack', + 'webpack.config.build.cjs': 'seti:webpack', + 'webpack.config.build.mjs': 'seti:webpack', + 'webpack.config.build.ts': 'seti:webpack', + 'webpack.common.js': 'seti:webpack', + 'webpack.common.cjs': 'seti:webpack', + 'webpack.common.mjs': 'seti:webpack', + 'webpack.common.ts': 'seti:webpack', + 'webpack.dev.js': 'seti:webpack', + 'webpack.dev.cjs': 'seti:webpack', + 'webpack.dev.mjs': 'seti:webpack', + 'webpack.dev.ts': 'seti:webpack', + 'webpack.prod.js': 'seti:webpack', + 'webpack.prod.cjs': 'seti:webpack', + 'webpack.prod.mjs': 'seti:webpack', + 'webpack.prod.ts': 'seti:webpack', + 'npm-debug.log': 'seti:npm_ignored', + }, + extensions: { + '.astro': 'astro', + '.mdx': 'mdx', + '.pkl': 'pkl', + '.bsl': 'seti:bsl', + '.mdo': 'seti:mdo', + '.cls': 'seti:salesforce', + '.apex': 'seti:salesforce', + '.asm': 'seti:asm', + '.s': 'seti:asm', + '.bicep': 'seti:bicep', + '.bzl': 'seti:bazel', + '.bazel': 'seti:bazel', + '.BUILD': 'seti:bazel', + '.WORKSPACE': 'seti:bazel', + '.bazelignore': 'seti:bazel', + '.bazelversion': 'seti:bazel', + '.c': 'seti:c', + '.h': 'seti:c', + '.m': 'seti:c', + '.cs': 'seti:c-sharp', + '.cshtml': 'seti:html', + '.aspx': 'seti:html', + '.ascx': 'seti:html', + '.asax': 'seti:html', + '.master': 'seti:html', + '.cc': 'seti:cpp', + '.cpp': 'seti:cpp', + '.cxx': 'seti:cpp', + '.c++': 'seti:cpp', + '.hh': 'seti:cpp', + '.hpp': 'seti:cpp', + '.hxx': 'seti:cpp', + '.h++': 'seti:cpp', + '.mm': 'seti:cpp', + '.clj': 'seti:clojure', + '.cljs': 'seti:clojure', + '.cljc': 'seti:clojure', + '.edn': 'seti:clojure', + '.cfc': 'seti:coldfusion', + '.cfm': 'seti:coldfusion', + '.coffee': 'seti:cjsx', + '.litcoffee': 'seti:cjsx', + '.config': 'seti:config', + '.cfg': 'seti:config', + '.conf': 'seti:config', + '.cr': 'seti:crystal', + '.ecr': 'seti:crystal_embedded', + '.slang': 'seti:crystal_embedded', + '.cson': 'seti:json', + '.css': 'seti:css', + '.css.map': 'seti:css', + '.sss': 'seti:css', + '.csv': 'seti:csv', + '.xls': 'seti:xls', + '.xlsx': 'seti:xls', + '.cu': 'seti:cu', + '.cuh': 'seti:cu', + '.hu': 'seti:cu', + '.cake': 'seti:cake', + '.ctp': 'seti:cake_php', + '.d': 'seti:d', + '.doc': 'seti:word', + '.docx': 'seti:word', + '.ejs': 'seti:ejs', + '.ex': 'seti:elixir', + '.exs': 'seti:elixir_script', + '.elm': 'seti:elm', + '.ico': 'seti:favicon', + '.fs': 'seti:f-sharp', + '.fsx': 'seti:f-sharp', + '.gitignore': 'seti:git', + '.gitconfig': 'seti:git', + '.gitkeep': 'seti:git', + '.gitattributes': 'seti:git', + '.gitmodules': 'seti:git', + '.go': 'seti:go', + '.slide': 'seti:go', + '.article': 'seti:go', + '.gd': 'seti:godot', + '.godot': 'seti:godot', + '.tres': 'seti:godot', + '.tscn': 'seti:godot', + '.gradle': 'seti:gradle', + '.groovy': 'seti:grails', + '.gsp': 'seti:grails', + '.gql': 'seti:graphql', + '.graphql': 'seti:graphql', + '.graphqls': 'seti:graphql', + '.hack': 'seti:hacklang', + '.haml': 'seti:haml', + '.handlebars': 'seti:mustache', + '.hbs': 'seti:mustache', + '.hjs': 'seti:mustache', + '.hs': 'seti:haskell', + '.lhs': 'seti:haskell', + '.hx': 'seti:haxe', + '.hxs': 'seti:haxe', + '.hxp': 'seti:haxe', + '.hxml': 'seti:haxe', + '.html': 'seti:html', + '.jade': 'seti:jade', + '.java': 'seti:java', + '.class': 'seti:java', + '.classpath': 'seti:java', + '.properties': 'seti:java', + '.js': 'seti:javascript', + '.js.map': 'seti:javascript', + '.cjs': 'seti:javascript', + '.cjs.map': 'seti:javascript', + '.mjs': 'seti:javascript', + '.mjs.map': 'seti:javascript', + '.spec.js': 'seti:javascript', + '.spec.cjs': 'seti:javascript', + '.spec.mjs': 'seti:javascript', + '.test.js': 'seti:javascript', + '.test.cjs': 'seti:javascript', + '.test.mjs': 'seti:javascript', + '.es': 'seti:javascript', + '.es5': 'seti:javascript', + '.es6': 'seti:javascript', + '.es7': 'seti:javascript', + '.jinja': 'seti:jinja', + '.jinja2': 'seti:jinja', + '.json': 'seti:json', + '.jl': 'seti:julia', + '.kt': 'seti:kotlin', + '.kts': 'seti:kotlin', + '.dart': 'seti:dart', + '.less': 'seti:json', + '.liquid': 'seti:liquid', + '.ls': 'seti:livescript', + '.lua': 'seti:lua', + '.markdown': 'seti:markdown', + '.md': 'seti:markdown', + '.argdown': 'seti:argdown', + '.ad': 'seti:argdown', + '.mustache': 'seti:mustache', + '.stache': 'seti:mustache', + '.nim': 'seti:nim', + '.nims': 'seti:nim', + '.github-issues': 'seti:github', + '.ipynb': 'seti:notebook', + '.njk': 'seti:nunjucks', + '.nunjucks': 'seti:nunjucks', + '.nunjs': 'seti:nunjucks', + '.nunj': 'seti:nunjucks', + '.njs': 'seti:nunjucks', + '.nj': 'seti:nunjucks', + '.npm-debug.log': 'seti:npm', + '.npmignore': 'seti:npm', + '.npmrc': 'seti:npm', + '.ml': 'seti:ocaml', + '.mli': 'seti:ocaml', + '.cmx': 'seti:ocaml', + '.cmxa': 'seti:ocaml', + '.odata': 'seti:odata', + '.pl': 'seti:perl', + '.php': 'seti:php', + '.php.inc': 'seti:php', + '.pipeline': 'seti:pipeline', + '.pddl': 'seti:pddl', + '.plan': 'seti:plan', + '.happenings': 'seti:happenings', + '.ps1': 'seti:powershell', + '.psd1': 'seti:powershell', + '.psm1': 'seti:powershell', + '.prisma': 'seti:prisma', + '.pug': 'seti:pug', + '.pp': 'seti:puppet', + '.epp': 'seti:puppet', + '.purs': 'seti:purescript', + '.py': 'seti:python', + '.jsx': 'seti:react', + '.spec.jsx': 'seti:react', + '.test.jsx': 'seti:react', + '.cjsx': 'seti:react', + '.tsx': 'seti:react', + '.spec.tsx': 'seti:react', + '.test.tsx': 'seti:react', + '.res': 'seti:rescript', + '.resi': 'seti:rescript', + '.R': 'seti:R', + '.rmd': 'seti:R', + '.rb': 'seti:ruby', + '.erb': 'seti:html', + '.erb.html': 'seti:html', + '.html.erb': 'seti:html', + '.rs': 'seti:rust', + '.sass': 'seti:sass', + '.scss': 'seti:sass', + '.springBeans': 'seti:spring', + '.slim': 'seti:slim', + '.smarty.tpl': 'seti:smarty', + '.tpl': 'seti:smarty', + '.sbt': 'seti:sbt', + '.scala': 'seti:scala', + '.sol': 'seti:ethereum', + '.styl': 'seti:stylus', + '.svelte': 'seti:svelte', + '.swift': 'seti:swift', + '.sql': 'seti:db', + '.soql': 'seti:db', + '.tf': 'seti:terraform', + '.tf.json': 'seti:terraform', + '.tfvars': 'seti:terraform', + '.tfvars.json': 'seti:terraform', + '.tex': 'seti:tex', + '.sty': 'seti:tex', + '.dtx': 'seti:tex', + '.ins': 'seti:tex', + '.txt': 'seti:default', + '.toml': 'seti:config', + '.twig': 'seti:twig', + '.ts': 'seti:typescript', + '.spec.ts': 'seti:typescript', + '.test.ts': 'seti:typescript', + '.vala': 'seti:vala', + '.vapi': 'seti:vala', + '.component': 'seti:html', + '.vue': 'seti:vue', + '.wasm': 'seti:wasm', + '.wat': 'seti:wat', + '.xml': 'seti:xml', + '.yml': 'seti:yml', + '.yaml': 'seti:yml', + '.pro': 'seti:prolog', + '.zig': 'seti:zig', + '.jar': 'seti:zip', + '.zip': 'seti:zip', + '.wgt': 'seti:wgt', + '.ai': 'seti:illustrator', + '.psd': 'seti:photoshop', + '.pdf': 'seti:pdf', + '.eot': 'seti:font', + '.ttf': 'seti:font', + '.woff': 'seti:font', + '.woff2': 'seti:font', + '.otf': 'seti:font', + '.avif': 'seti:image', + '.gif': 'seti:image', + '.jpg': 'seti:image', + '.jpeg': 'seti:image', + '.png': 'seti:image', + '.pxm': 'seti:image', + '.svg': 'seti:svg', + '.svgx': 'seti:image', + '.tiff': 'seti:image', + '.webp': 'seti:image', + '.sublime-project': 'seti:sublime', + '.sublime-workspace': 'seti:sublime', + '.code-search': 'seti:code-search', + '.sh': 'seti:shell', + '.zsh': 'seti:shell', + '.fish': 'seti:shell', + '.zshrc': 'seti:shell', + '.bashrc': 'seti:shell', + '.mov': 'seti:video', + '.ogv': 'seti:video', + '.webm': 'seti:video', + '.avi': 'seti:video', + '.mpg': 'seti:video', + '.mp4': 'seti:video', + '.mp3': 'seti:audio', + '.ogg': 'seti:audio', + '.wav': 'seti:audio', + '.flac': 'seti:audio', + '.3ds': 'seti:svg', + '.3dm': 'seti:svg', + '.stl': 'seti:svg', + '.obj': 'seti:svg', + '.dae': 'seti:svg', + '.bat': 'seti:windows', + '.cmd': 'seti:windows', + '.babelrc': 'seti:babel', + '.babelrc.js': 'seti:babel', + '.babelrc.cjs': 'seti:babel', + '.bazelrc': 'seti:bazel', + '.bowerrc': 'seti:bower', + '.codeclimate.yml': 'seti:code-climate', + '.eslintrc': 'seti:eslint', + '.eslintrc.js': 'seti:eslint', + '.eslintrc.cjs': 'seti:eslint', + '.eslintrc.yaml': 'seti:eslint', + '.eslintrc.yml': 'seti:eslint', + '.eslintrc.json': 'seti:eslint', + '.eslintignore': 'seti:eslint', + '.firebaserc': 'seti:firebase', + '.gitlab-ci.yml': 'seti:gitlab', + '.jshintrc': 'seti:javascript', + '.jscsrc': 'seti:javascript', + '.stylelintrc': 'seti:stylelint', + '.stylelintrc.json': 'seti:stylelint', + '.stylelintrc.yaml': 'seti:stylelint', + '.stylelintrc.yml': 'seti:stylelint', + '.stylelintrc.js': 'seti:stylelint', + '.stylelintignore': 'seti:stylelint', + '.direnv': 'seti:config', + '.env': 'seti:config', + '.static': 'seti:config', + '.editorconfig': 'seti:config', + '.slugignore': 'seti:config', + '.tmp': 'seti:clock', + '.htaccess': 'seti:config', + '.key': 'seti:lock', + '.cert': 'seti:lock', + '.cer': 'seti:lock', + '.crt': 'seti:lock', + '.pem': 'seti:lock', + '.DS_Store': 'seti:ignored', + }, + partials: { + 'mix': 'seti:hex', + 'Gemfile': 'seti:ruby', + 'gemfile': 'seti:ruby', + 'dockerfile': 'seti:docker', + 'Dockerfile': 'seti:docker', + 'DOCKERFILE': 'seti:docker', + '.dockerignore': 'seti:docker', + 'docker-healthcheck': 'seti:docker', + 'docker-compose.yml': 'seti:docker', + 'docker-compose.yaml': 'seti:docker', + 'docker-compose.override.yml': 'seti:docker', + 'docker-compose.override.yaml': 'seti:docker', + 'GULPFILE': 'seti:gulp', + 'Gulpfile': 'seti:gulp', + 'gulpfile': 'seti:gulp', + 'gulpfile.js': 'seti:gulp', + 'LICENSE': 'seti:license', + 'LICENCE': 'seti:license', + 'LICENSE.txt': 'seti:license', + 'LICENCE.txt': 'seti:license', + 'LICENSE.md': 'seti:license', + 'LICENCE.md': 'seti:license', + 'COPYING': 'seti:license', + 'COPYING.txt': 'seti:license', + 'COPYING.md': 'seti:license', + 'COMPILING': 'seti:license', + 'COMPILING.txt': 'seti:license', + 'COMPILING.md': 'seti:license', + 'CONTRIBUTING': 'seti:license', + 'CONTRIBUTING.txt': 'seti:license', + 'CONTRIBUTING.md': 'seti:license', + 'MAKEFILE': 'seti:makefile', + 'Makefile': 'seti:makefile', + 'makefile': 'seti:makefile', + 'QMAKEFILE': 'seti:makefile', + 'QMakefile': 'seti:makefile', + 'qmakefile': 'seti:makefile', + 'OMAKEFILE': 'seti:makefile', + 'OMakefile': 'seti:makefile', + 'omakefile': 'seti:makefile', + 'CMAKELISTS.TXT': 'seti:makefile', + 'CMAKELISTS.txt': 'seti:makefile', + 'CMakeLists.txt': 'seti:makefile', + 'cmakelists.txt': 'seti:makefile', + 'Procfile': 'seti:heroku', + 'TODO': 'seti:todo', + 'TODO.txt': 'seti:todo', + 'TODO.md': 'seti:todo', + }, +} + +export const FileIcons = { + 'pnpm': '', + 'biome': + '', + 'bun': '', + + 'seti:folder': + '', + 'seti:bsl': + '', + 'seti:mdo': + '', + 'seti:salesforce': + '', + 'seti:asm': + '', + 'seti:bicep': + '', + 'seti:bazel': + '', + 'seti:c': + '', + 'seti:c-sharp': + '', + 'seti:html': + '', + 'seti:cpp': + '', + 'seti:clojure': + '', + 'seti:coldfusion': + '', + 'seti:config': + '', + 'seti:crystal': + '', + 'seti:crystal_embedded': + '', + 'seti:json': + '', + 'seti:css': + '', + 'seti:csv': + '', + 'seti:xls': + '', + 'seti:cu': + '', + 'seti:cake': + '', + 'seti:cake_php': + '', + 'seti:d': + '', + 'seti:word': + '', + 'seti:elixir': + '', + 'seti:elixir_script': + '', + 'seti:hex': + '', + 'seti:elm': + '', + 'seti:favicon': + '', + 'seti:f-sharp': + '', + 'seti:git': + '', + 'seti:go': + '', + 'seti:godot': + '', + 'seti:gradle': + '', + 'seti:grails': + '', + 'seti:graphql': + '', + 'seti:hacklang': + '', + 'seti:haml': + '', + 'seti:mustache': + '', + 'seti:haskell': + '', + 'seti:haxe': + '', + 'seti:jade': + '', + 'seti:java': + '', + 'seti:javascript': + '', + 'seti:jinja': + '', + 'seti:julia': + '', + 'seti:karma': + '', + 'seti:kotlin': + '', + 'seti:dart': + '', + 'seti:liquid': + '', + 'seti:livescript': + '', + 'seti:lua': + '', + 'seti:markdown': + '', + 'seti:argdown': + '', + 'seti:info': + '', + 'seti:clock': + '', + 'seti:maven': + '', + 'seti:nim': + '', + 'seti:github': + '', + 'seti:notebook': + '', + 'seti:nunjucks': + '', + 'seti:npm': + '', + 'seti:ocaml': + '', + 'seti:odata': + '', + 'seti:perl': + '', + 'seti:php': + '', + 'seti:pipeline': + '', + 'seti:pddl': + '', + 'seti:plan': + '', + 'seti:happenings': + '', + 'seti:powershell': + '', + 'seti:prisma': + '', + 'seti:pug': + '', + 'seti:puppet': + '', + 'seti:purescript': + '', + 'seti:python': + '', + 'seti:react': + '', + 'seti:rescript': + '', + 'seti:R': + '', + 'seti:ruby': + '', + 'seti:rust': + '', + 'seti:sass': + '', + 'seti:spring': + '', + 'seti:slim': + '', + 'seti:smarty': + '', + 'seti:sbt': + '', + 'seti:scala': + '', + 'seti:ethereum': + '', + 'seti:stylus': + '', + 'seti:svelte': + '', + 'seti:swift': + '', + 'seti:db': + '', + 'seti:terraform': + '', + 'seti:tex': + '', + 'seti:default': + '', + 'seti:twig': + '', + 'seti:typescript': + '', + 'seti:tsconfig': + '', + 'seti:vala': + '', + 'seti:vue': + '', + 'seti:wasm': + '', + 'seti:wat': + '', + 'seti:xml': + '', + 'seti:yml': + '', + 'seti:prolog': + '', + 'seti:zig': + '', + 'seti:zip': + '', + 'seti:wgt': + '', + 'seti:illustrator': + '', + 'seti:photoshop': + '', + 'seti:pdf': + '', + 'seti:font': + '', + 'seti:image': + '', + 'seti:svg': + '', + 'seti:sublime': + '', + 'seti:code-search': + '', + 'seti:shell': + '', + 'seti:video': + '', + 'seti:audio': + '', + 'seti:windows': + '', + 'seti:jenkins': + '', + 'seti:babel': + '', + 'seti:bower': + '', + 'seti:docker': + '', + 'seti:code-climate': + '', + 'seti:eslint': + '', + 'seti:firebase': + '', + 'seti:firefox': + '', + 'seti:gitlab': + '', + 'seti:grunt': + '', + 'seti:gulp': + '', + 'seti:ionic': + '', + 'seti:platformio': + '', + 'seti:rollup': + '', + 'seti:stylelint': + '', + 'seti:yarn': + '', + 'seti:webpack': + '', + 'seti:lock': + '', + 'seti:license': + '', + 'seti:makefile': + '', + 'seti:heroku': + '', + 'seti:todo': + '', + 'seti:ignored': + '', +} diff --git a/plugins/plugin-md-power/src/node/features/fileTree/index.ts b/plugins/plugin-md-power/src/node/features/fileTree/index.ts new file mode 100644 index 00000000..ee75a8f2 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/fileTree/index.ts @@ -0,0 +1,77 @@ +import fs from 'node:fs' +import container from 'markdown-it-container' +import type { Markdown } from 'vuepress/markdown' +import type Token from 'markdown-it/lib/token.mjs' +import type { App } from 'vuepress/core' +import { resolveTreeNodeInfo, updateInlineToken } from './resolveTreeNodeInfo.js' +import { type FileIcon, folderIcon, getFileIcon } from './findIcon.js' + +const type = 'file-tree' +const closeType = `container_${type}_close` +const componentName = 'FileTreeItem' +const itemOpen = 'file_tree_item_open' +const itemClose = 'file_tree_item_close' +const classPrefix = 'vp-fti-' +const styleFilepath = 'internal/md-power/file-tree.css' + +export async function fileTreePlugin(app: App, md: Markdown) { + const validate = (info: string): boolean => info.trim().startsWith(type) + const render = (tokens: Token[], idx: number): string => { + if (tokens[idx].nesting === 1) { + for ( + let i = idx + 1; + !(tokens[i].nesting === -1 + && tokens[i].type === closeType); + ++i + ) { + const token = tokens[i] + if (token.type === 'list_item_open') { + const result = resolveTreeNodeInfo(tokens, token, i) + if (result) { + const [info, inline] = result + const { filename, type, expanded, empty } = info + const icon = type === 'file' ? getFileIcon(filename) : folderIcon + + token.type = itemOpen + token.tag = componentName + token.attrSet('type', type) + token.attrSet(':expanded', expanded ? 'true' : 'false') + token.attrSet(':empty', empty ? 'true' : 'false') + updateInlineToken(inline, info, `${classPrefix}${icon.name}`) + addIcon(icon) + } + } + else if (token.type === 'list_item_close') { + token.type = itemClose + token.tag = componentName + } + } + return '
' + } + else { + return '
' + } + } + + let timer: NodeJS.Timeout | null = null + const icons: Record = {} + + function addIcon(icon: FileIcon) { + icons[icon.name] = icon + + if (timer) + clearTimeout(timer) + timer = setTimeout(async () => { + let content = '' + for (const icon of Object.values(icons)) { + content += `.${classPrefix}${icon.name} { --icon: ${icon.svg}; }\n` + } + await app.writeTemp(styleFilepath, content) + }, 300) + } + + md.use(container, type, { validate, render }) + + if (!fs.existsSync(app.dir.temp(styleFilepath))) + await app.writeTemp(styleFilepath, '') +} diff --git a/plugins/plugin-md-power/src/node/features/fileTree/resolveTreeNodeInfo.ts b/plugins/plugin-md-power/src/node/features/fileTree/resolveTreeNodeInfo.ts new file mode 100644 index 00000000..334dec76 --- /dev/null +++ b/plugins/plugin-md-power/src/node/features/fileTree/resolveTreeNodeInfo.ts @@ -0,0 +1,125 @@ +import { removeLeadingSlash } from 'vuepress/shared' +import Token from 'markdown-it/lib/token.mjs' + +interface FileTreeNode { + filename: string + type: 'folder' | 'file' + expanded: boolean + focus: boolean + empty: boolean +} + +export function resolveTreeNodeInfo( + tokens: Token[], + current: Token, + idx: number, +): [FileTreeNode, Token] | undefined { + let hasInline = false + let hasChildren = false + let inline!: Token + for ( + let i = idx + 1; + !(tokens[i].level === current.level && tokens[i].type === 'list_item_close'); + ++i + ) { + if (tokens[i].type === 'inline' && !hasInline) { + inline = tokens[i] + hasInline = true + } + else if (tokens[i].tag === 'ul') { + hasChildren = true + } + + if (hasInline && hasChildren) + break + } + + if (!hasInline) + return undefined + + const children = inline.children?.filter(token => (token.type === 'text' && token.content) || token.tag === 'strong') || [] + const filename = children.filter(token => token.type === 'text').map(token => token.content).join(' ').split(/\s+/)[0] ?? '' + const focus = children[0].tag === 'strong' + const type = hasChildren || filename.endsWith('/') ? 'folder' : 'file' + const info: FileTreeNode = { + filename: removeLeadingSlash(filename), + type, + focus, + empty: !hasChildren, + expanded: type === 'folder' && !filename.endsWith('/'), + } + + return [info, inline] as const +} + +export function updateInlineToken(inline: Token, info: FileTreeNode, icon: string) { + const children = inline.children + if (!children) + return + + const tokens: Token[] = [] + const wrapperOpen = new Token('span_open', 'span', 1) + const wrapperClose = new Token('span_close', 'span', -1) + + wrapperOpen.attrSet('class', `tree-node ${info.type}`) + tokens.push(wrapperOpen) + + if (info.filename !== '...' && info.filename !== '…') { + const iconOpen = new Token('span_open', 'span', 1) + iconOpen.attrSet('class', icon) + const iconClose = new Token('span_close', 'span', -1) + + tokens.push(iconOpen, iconClose) + } + + const fileOpen = new Token('span_open', 'span', 1) + fileOpen.attrSet('class', `name${info.focus ? ' focus' : ''}`) + tokens.push(fileOpen) + + let isStrongTag = false + while (children.length) { + const token = children.shift()! + if (token.type === 'text' && token.content) { + if (token.content.includes(' ')) { + const [first, ...other] = token.content.split(' ') + const text = new Token('text', '', 0) + text.content = first + tokens.push(text) + const comment = new Token('text', '', 0) + comment.content = other.join(' ') + children.unshift(comment) + } + else { + tokens.push(token) + } + if (!isStrongTag) + break + } + else if (token.tag === 'strong') { + tokens.push(token) + if (token.nesting === 1) { + isStrongTag = true + } + else { + break + } + } + else { + tokens.push(token) + } + } + + const fileClose = new Token('span_close', 'span', -1) + tokens.push(fileClose) + + if (children.filter(token => token.type === 'text' && token.content.trim()).length) { + const commentOpen = new Token('span_open', 'span', 1) + commentOpen.attrSet('class', 'comment') + const commentClose = new Token('span_close', 'span', -1) + + tokens.push(commentOpen, ...children, commentClose) + } + + tokens.push(wrapperClose) + inline.children = tokens +} diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts index 2f3f5714..92a63bd7 100644 --- a/plugins/plugin-md-power/src/node/plugin.ts +++ b/plugins/plugin-md-power/src/node/plugin.ts @@ -14,6 +14,7 @@ import { jsfiddlePlugin } from './features/jsfiddle.js' import { plotPlugin } from './features/plot.js' import { langReplPlugin } from './features/langRepl.js' import { prepareConfigFile } from './prepareConfigFile.js' +import { fileTreePlugin } from './features/fileTree/index.js' export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin { return (app) => { @@ -89,12 +90,17 @@ export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): P options.plot === true || (typeof options.plot === 'object' && options.plot.tag !== false) ) { - // =|plot|= + // !!plot!! md.use(plotPlugin) } if (options.repl) await langReplPlugin(app, md, options.repl) + + if (options.fileTree) { + // ::: file-tree + await fileTreePlugin(app, md) + } }, } } diff --git a/plugins/plugin-md-power/src/node/prepareConfigFile.ts b/plugins/plugin-md-power/src/node/prepareConfigFile.ts index 7c39314b..72287c04 100644 --- a/plugins/plugin-md-power/src/node/prepareConfigFile.ts +++ b/plugins/plugin-md-power/src/node/prepareConfigFile.ts @@ -54,6 +54,12 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp enhances.add(`app.component('CanIUseViewer', CanIUse)`) } + if (options.fileTree) { + imports.add(`import FileTreeItem from '${CLIENT_FOLDER}components/FileTreeItem.vue'`) + imports.add(`import '@internal/md-power/file-tree.css'`) + enhances.add(`app.component('FileTreeItem', FileTreeItem)`) + } + return app.writeTemp( 'md-power/config.js', `\ diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts index 6c51f38b..a9c5367d 100644 --- a/plugins/plugin-md-power/src/shared/plugin.ts +++ b/plugins/plugin-md-power/src/shared/plugin.ts @@ -26,6 +26,7 @@ export interface MarkdownPowerPluginOptions { // container repl?: false | ReplOptions + fileTree?: boolean caniuse?: boolean | CanIUseOptions } diff --git a/plugins/plugin-md-power/tsup.config.ts b/plugins/plugin-md-power/tsup.config.ts index 3647ccf9..921bbaaf 100644 --- a/plugins/plugin-md-power/tsup.config.ts +++ b/plugins/plugin-md-power/tsup.config.ts @@ -33,6 +33,7 @@ export default defineConfig(() => { entry: ['./src/node/index.ts'], outDir: './lib/node', target: 'node18', + external: ['markdown-it'], }, // client ...config.map(({ dir, files }) => ({ diff --git a/theme/src/node/plugins/getPlugins.ts b/theme/src/node/plugins/getPlugins.ts index 0721f7e5..3a1153c0 100644 --- a/theme/src/node/plugins/getPlugins.ts +++ b/theme/src/node/plugins/getPlugins.ts @@ -131,6 +131,9 @@ export function getPlugins({ if (pluginOptions.markdownPower !== false) { plugins.push(markdownPowerPlugin({ caniuse: pluginOptions.caniuse, + fileTree: true, + plot: true, + icons: true, ...pluginOptions.markdownPower || {}, repl: pluginOptions.markdownPower?.repl ? { theme: shikiTheme, ...pluginOptions.markdownPower?.repl }