diff --git a/lib/models/code.ts b/lib/models/code.ts new file mode 100644 index 00000000..bbe02707 --- /dev/null +++ b/lib/models/code.ts @@ -0,0 +1,20 @@ +import warehouse from 'warehouse'; +import type Hexo from '../hexo'; +import { CodeSchema } from '../types'; +import { join } from 'path'; + +export = (ctx: Hexo) => { + const Code = new warehouse.Schema({ + _id: { type: String, required: true }, + path: { type: String, required: true }, + slug: { type: String, required: true }, + modified: { type: Boolean, default: true }, + content: { type: String, default: '' } + }); + + Code.virtual('source').get(function() { + return join(ctx.base_dir, this._id); + }); + + return Code; +}; diff --git a/lib/models/index.ts b/lib/models/index.ts index d76aff60..1fef061a 100644 --- a/lib/models/index.ts +++ b/lib/models/index.ts @@ -8,3 +8,4 @@ export { default as PostAsset } from './post_asset'; export { default as PostCategory } from './post_category'; export { default as PostTag } from './post_tag'; export { default as Tag } from './tag'; +export { default as Code } from './code'; diff --git a/lib/plugins/generator/asset.ts b/lib/plugins/generator/asset.ts index 4b5a2ddb..c6898e48 100644 --- a/lib/plugins/generator/asset.ts +++ b/lib/plugins/generator/asset.ts @@ -3,7 +3,7 @@ import Promise from 'bluebird'; import { extname } from 'path'; import { magenta } from 'picocolors'; import type Hexo from '../../hexo'; -import type { AssetSchema, BaseGeneratorReturn } from '../../types'; +import type { AssetSchema, BaseGeneratorReturn, PostAssetSchema } from '../../types'; import type Document from 'warehouse/dist/document'; interface AssetData { @@ -19,9 +19,9 @@ interface AssetGenerator extends BaseGeneratorReturn { } const process = (name: string, ctx: Hexo) => { - return Promise.filter(ctx.model(name).toArray(), (asset: Document) => exists(asset.source).tap(exist => { + return Promise.filter(ctx.model(name).toArray(), (asset: Document | Document) => exists(asset.source).tap(exist => { if (!exist) return asset.remove(); - })).map((asset: Document) => { + })).map((asset: Document | Document) => { const { source } = asset; let { path } = asset; const data: AssetData = { diff --git a/lib/plugins/generator/code.ts b/lib/plugins/generator/code.ts new file mode 100644 index 00000000..c838090f --- /dev/null +++ b/lib/plugins/generator/code.ts @@ -0,0 +1,27 @@ +import type Hexo from '../../hexo'; +import Promise from 'bluebird'; +import { exists } from 'hexo-fs'; +import { CodeSchema } from '../../types'; +import type Document from 'warehouse/dist/document'; + +interface CodeData { + modified: boolean; + data: string; +} + +function codeGenerator(this: Hexo): Promise { + return Promise.filter(this.model('Code').toArray(), (code: Document) => exists(code.source).tap(exist => { + if (!exist) return code.remove(); + })).map((code: Document) => { + const { path } = code; + const data: CodeData = { + modified: code.modified, + data: code.content + }; + + return { path, data }; + }); + +} + +export = codeGenerator; diff --git a/lib/plugins/generator/index.ts b/lib/plugins/generator/index.ts index 9c653630..53fd09ec 100644 --- a/lib/plugins/generator/index.ts +++ b/lib/plugins/generator/index.ts @@ -3,6 +3,7 @@ import type Hexo from '../../hexo'; export = (ctx: Hexo) => { const { generator } = ctx.extend; + generator.register('code', require('./code')); generator.register('asset', require('./asset')); generator.register('page', require('./page')); generator.register('post', require('./post')); diff --git a/lib/plugins/processor/asset.ts b/lib/plugins/processor/asset.ts index b3282b93..92863a9e 100644 --- a/lib/plugins/processor/asset.ts +++ b/lib/plugins/processor/asset.ts @@ -10,9 +10,12 @@ import type { Stats } from 'fs'; import { PageSchema } from '../../types'; export = (ctx: Hexo) => { + let codeDir = ctx.config.code_dir; + if (!codeDir.endsWith('/')) codeDir += '/'; return { pattern: new Pattern(path => { if (isExcludedFile(path, ctx.config)) return; + if (path.startsWith(codeDir)) return; return { renderable: ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render) diff --git a/lib/plugins/processor/code.ts b/lib/plugins/processor/code.ts new file mode 100644 index 00000000..2ed5c248 --- /dev/null +++ b/lib/plugins/processor/code.ts @@ -0,0 +1,42 @@ +import { Pattern } from 'hexo-util'; +import { relative } from 'path'; +import type Hexo from '../../hexo'; +import type { _File } from '../../box'; + +export = (ctx: Hexo) => { + let codeDir = ctx.config.code_dir; + if (!codeDir.endsWith('/')) codeDir += '/'; + return { + pattern: new Pattern(path => { + return path.startsWith(codeDir); + }), + process: function codeProcessor(file: _File) { + const id = relative(ctx.base_dir, file.source).replace(/\\/g, '/'); + const slug = relative(ctx.config.source_dir, id).replace(/\\/g, '/'); + const Code = ctx.model('Code'); + const doc = Code.findById(id); + + if (file.type === 'delete') { + if (doc) { + return doc.remove(); + } + + return; + } + + if (file.type === 'skip' && doc) { + return; + } + + return file.read().then(content => { + return Code.save({ + _id: id, + path: file.path, + slug, + modified: file.type !== 'skip', + content + }); + }); + } + }; +}; diff --git a/lib/plugins/processor/index.ts b/lib/plugins/processor/index.ts index e7ae2c58..ce4f942b 100644 --- a/lib/plugins/processor/index.ts +++ b/lib/plugins/processor/index.ts @@ -8,6 +8,7 @@ export = (ctx: Hexo) => { processor.register(obj.pattern, obj.process); } + register('code'); register('asset'); register('data'); register('post'); diff --git a/lib/plugins/tag/include_code.ts b/lib/plugins/tag/include_code.ts index 8371860c..345777ca 100644 --- a/lib/plugins/tag/include_code.ts +++ b/lib/plugins/tag/include_code.ts @@ -50,8 +50,8 @@ export = (ctx: Hexo) => function includeCodeTag(args: string[]) { const source = join(codeDir, path).replace(/\\/g, '/'); // Prevent path traversal: https://github.com/hexojs/hexo/issues/5250 - const Page = ctx.model('Page'); - const doc = Page.findOne({ source }); + const Code = ctx.model('Code'); + const doc = Code.findOne({ slug: source }); if (!doc) return; let code = doc.content; diff --git a/lib/types.ts b/lib/types.ts index 3d78ac54..21227f81 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -346,6 +346,15 @@ export interface PageSchema extends BasePagePostSchema { tag?: string; } +export interface CodeSchema { + _id: string; + path: string; + slug: string; + modified: boolean; + content: string; + source: string; +} + export interface AssetSchema { _id?: string; path: string; diff --git a/test/scripts/generators/code.ts b/test/scripts/generators/code.ts new file mode 100644 index 00000000..cbc021f9 --- /dev/null +++ b/test/scripts/generators/code.ts @@ -0,0 +1,110 @@ +import { join } from 'path'; +import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; +import Hexo from '../../../lib/hexo'; +import codeGenerator from '../../../lib/plugins/generator/code'; +import defaults from '../../../lib/hexo/default_config'; +import chai from 'chai'; +const should = chai.should(); +type CodeParams = Parameters +type CodeReturn = ReturnType + +describe('code', () => { + const hexo = new Hexo(join(__dirname, 'code_test'), {silent: true}); + const generator: (...args: CodeParams) => CodeReturn = codeGenerator.bind(hexo); + const Code = hexo.model('Code'); + const codeDir = defaults.code_dir; + + before(async () => { + await mkdirs(hexo.base_dir); + await hexo.init(); + }); + + after(() => rmdir(hexo.base_dir)); + + it('renderable', async () => { + const path = 'test.j2'; + const source = join(hexo.base_dir, defaults.source_dir, defaults.code_dir, path); + const content = '{{ 1 }}'; + + await Promise.all([ + Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/${path}`, + slug: `${codeDir}/${path}`, + path: `${codeDir}/${path}`, + content + }), + writeFile(source, content) + ]); + const data = await generator(); + data[0].path.should.eql(`${codeDir}/${path}`); + data[0].data.modified.should.be.true; + + const result = await data[0].data.data; + result.should.eql(content); + + await Promise.all([ + Code.removeById(`${defaults.source_dir}/${codeDir}/${path}`), + unlink(source) + ]); + }); + + it('not renderable', async () => { + const path = 'test.txt'; + const source = join(hexo.base_dir, defaults.source_dir, defaults.code_dir, path); + const content = 'test content'; + + await Promise.all([ + Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/${path}`, + slug: `${codeDir}/${path}`, + path: `${codeDir}/${path}`, + content + }), + writeFile(source, content) + ]); + const data = await generator(); + data[0].path.should.eql(`${codeDir}/${path}`); + data[0].data.modified.should.be.true; + + const result = await data[0].data.data; + result.should.eql(content); + + await Promise.all([ + Code.removeById(`${defaults.source_dir}/${codeDir}/${path}`), + unlink(source) + ]); + }); + + it('remove codes which does not exist', async () => { + const path = 'test.js'; + + await Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/${path}`, + slug: `${codeDir}/${path}`, + path: `${codeDir}/${path}` + }); + await generator(); + should.not.exist(Code.findById(`${defaults.source_dir}/${codeDir}/${path}`)); + }); + + it('don\'t remove extension name', async () => { + const path = 'test.min.js'; + const source = join(hexo.base_dir, defaults.source_dir, defaults.code_dir, path); + + await Promise.all([ + Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/${path}`, + slug: `${codeDir}/${path}`, + path: `${codeDir}/${path}` + }), + writeFile(source, '') + ]); + const data = await generator(); + data[0].path.should.eql(`${codeDir}/${path}`); + + await Promise.all([ + Code.removeById(`${defaults.source_dir}/${codeDir}/${path}`), + unlink(source) + ]); + }); +}); diff --git a/test/scripts/models/code.ts b/test/scripts/models/code.ts new file mode 100644 index 00000000..e100bd79 --- /dev/null +++ b/test/scripts/models/code.ts @@ -0,0 +1,59 @@ +import { join } from 'path'; +import Hexo from '../../../lib/hexo'; + +describe('Code', () => { + const hexo = new Hexo(); + const Code = hexo.model('Code'); + + it('_id - required', async () => { + try { + await Code.insert({}); + } catch (err) { + err.message.should.eql('ID is not defined'); + } + }); + + it('path - required', async () => { + try { + await Code.insert({ + _id: 'foo' + }); + } catch (err) { + err.message.should.eql('`path` is required!'); + } + }); + + it('slug - required', async () => { + try { + await Code.insert({ + _id: 'foo', + path: 'bar' + }); + } catch (err) { + err.message.should.eql('`slug` is required!'); + } + }); + + it('default values', async () => { + const data = await Code.insert({ + _id: 'foo', + path: 'bar', + slug: 'baz' + }); + data.modified.should.be.true; + data.content.should.eql(''); + + Code.removeById(data._id); + }); + + it('source - virtual', async () => { + const data = await Code.insert({ + _id: 'foo', + path: 'bar', + slug: 'baz' + }); + data.source.should.eql(join(hexo.base_dir, data._id)); + + Code.removeById(data._id); + }); +}); diff --git a/test/scripts/processors/code.ts b/test/scripts/processors/code.ts new file mode 100644 index 00000000..90cd915c --- /dev/null +++ b/test/scripts/processors/code.ts @@ -0,0 +1,155 @@ +import { join } from 'path'; +import { mkdirs, rmdir, unlink, writeFile } from 'hexo-fs'; +import Hexo from '../../../lib/hexo'; +import defaults from '../../../lib/hexo/default_config'; +import codes from '../../../lib/plugins/processor/code'; +import chai from 'chai'; +import BluebirdPromise from 'bluebird'; +const should = chai.should(); + +describe('code', () => { + const baseDir = join(__dirname, 'code_test'); + const hexo = new Hexo(baseDir); + const code = codes(hexo); + const process = BluebirdPromise.method(code.process).bind(hexo); + const { pattern } = code; + const { source } = hexo; + const { File } = source; + const Code = hexo.model('Code'); + const codeDir = defaults.code_dir; + + function newFile(options) { + const path = options.path; + + options.params = { + path + }; + + options.path = `${codeDir}/${path}`; + options.source = join(source.base, options.path); + + return new File(options); + } + + before(async () => { + await mkdirs(baseDir); + await hexo.init(); + }); + + beforeEach(() => { hexo.config = Object.assign({}, defaults); }); + + after(() => rmdir(baseDir)); + + it('pattern', () => { + pattern.match(`${codeDir}/users.json`).should.eql(true); + pattern.match(`${codeDir}/users.j2`).should.eql(true); + pattern.match(`${codeDir}/users.html`).should.eql(true); + pattern.match('users.json').should.eql(false); + }); + + it('type: create - renderable', async () => { + const body = '{{ 1 }}'; + + const file = newFile({ + path: 'users.j2', + type: 'create' + }); + + await writeFile(file.source, body); + await process(file); + const data = Code.findOne({ slug: `${codeDir}/users.j2` }); + + data.content.should.eql('{{ 1 }}'); + + data.remove(); + unlink(file.source); + }); + + it('type: create - non-renderable', async () => { + const body = '{a: 1}'; + + const file = newFile({ + path: 'users.txt', + type: 'create' + }); + + await writeFile(file.source, body); + await process(file); + const data = Code.findOne({ slug: `${codeDir}/users.txt` }); + + data.content.should.eql('{a: 1}'); + + data.remove(); + unlink(file.source); + }); + + it('type: update', async () => { + const body = '{{ 1 }}'; + + const file = newFile({ + path: 'users.j2', + type: 'update' + }); + + await BluebirdPromise.all([ + writeFile(file.source, body), + Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/users.j2`, + slug: `${codeDir}/users.j2`, + path: `${codeDir}/users.j2`, + content: '' + }) + ]); + await process(file); + const data = Code.findOne({ slug: `${codeDir}/users.j2` }); + + data.content.should.eql('{{ 1 }}'); + + data.remove(); + unlink(file.source); + }); + + it('type: skip', async () => { + const file = newFile({ + path: 'users.j2', + type: 'skip' + }); + + await Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/users.j2`, + slug: `${codeDir}/users.j2`, + path: `${codeDir}/users.j2`, + content: '{{ 1 }}' + }); + const data = Code.findOne({ slug: `${codeDir}/users.j2` }); + await process(file); + should.exist(data); + data.remove(); + }); + + it('type: delete', async () => { + const file = newFile({ + path: 'users.j2', + type: 'delete' + }); + + await Code.insert({ + _id: `${defaults.source_dir}/${codeDir}/users.j2`, + slug: `${codeDir}/users.j2`, + path: `${codeDir}/users.j2`, + content: '{{ 1 }}' + }); + await process(file); + should.not.exist(Code.findOne({ slug: `${codeDir}/users.j2` })); + }); + + it('type: delete - not exist', async () => { + const file = newFile({ + path: 'users.j2', + type: 'delete' + }); + + await process(file); + should.not.exist(Code.findOne({ slug: `${codeDir}/users.j2` })); + }); +}); diff --git a/test/scripts/tags/include_code.ts b/test/scripts/tags/include_code.ts index d82e7aee..faf89fca 100644 --- a/test/scripts/tags/include_code.ts +++ b/test/scripts/tags/include_code.ts @@ -7,7 +7,7 @@ import tagIncludeCode from '../../../lib/plugins/tag/include_code'; import chai from 'chai'; const should = chai.should(); -describe('include_code', () => { +describe('include_code_js', () => { const hexo = new Hexo(join(__dirname, 'include_code_test')); require('../../../lib/plugins/highlight/')(hexo); const includeCode = BluebirdPromise.method(tagIncludeCode(hexo)) as (arg1: string[]) => BluebirdPromise; @@ -244,3 +244,39 @@ describe('include_code', () => { }); }); }); + +describe('include_code_j2', () => { + const hexo = new Hexo(join(__dirname, 'include_code_test')); + require('../../../lib/plugins/highlight/')(hexo); + const includeCode = BluebirdPromise.method(tagIncludeCode(hexo)) as (arg1: string[]) => BluebirdPromise; + const path = join(hexo.source_dir, hexo.config.code_dir, 'test.j2'); + const defaultCfg = JSON.parse(JSON.stringify(hexo.config)); + + const fixture = [ + '{{ 1 }}' + ].join('\n'); + + const code = args => includeCode(args.split(' ')); + + before(async () => { + await writeFile(path, fixture); + await hexo.init(); + await hexo.load(); + }); + + beforeEach(() => { + hexo.config = JSON.parse(JSON.stringify(defaultCfg)); + }); + + after(() => rmdir(hexo.base_dir)); + + it('default', async () => { + const expected = highlight(fixture, { + lang: 'jinja2', + caption: 'test.j2view raw' + }); + + const result = await code('test.j2'); + result.should.eql(expected); + }); +});