Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/models/code.ts
Original file line number Diff line number Diff line change
@@ -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<CodeSchema>({
_id: { type: String, required: true },
path: { type: String, required: true },
slug: { type: String, required: true },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the same value is already set in path, I don't think slug is necessary. Though the difference may be slight, reducing the elements in Schema<CodeSchema> should have a positive effect on performance.

However, if we remove the slug, it could potentially be a breaking change since there may be users referencing it in custom scripts or etc..., so it might require an announcement.

modified: { type: Boolean, default: true },
content: { type: String, default: '' }
});

Code.virtual('source').get(function() {
return join(ctx.base_dir, this._id);
});

return Code;
};
1 change: 1 addition & 0 deletions lib/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
6 changes: 3 additions & 3 deletions lib/plugins/generator/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,9 +19,9 @@ interface AssetGenerator extends BaseGeneratorReturn {
}

const process = (name: string, ctx: Hexo) => {
return Promise.filter(ctx.model(name).toArray(), (asset: Document<AssetSchema>) => exists(asset.source).tap(exist => {
return Promise.filter(ctx.model(name).toArray(), (asset: Document<AssetSchema> | Document<PostAssetSchema>) => exists(asset.source).tap(exist => {
if (!exist) return asset.remove();
})).map((asset: Document<AssetSchema>) => {
})).map((asset: Document<AssetSchema> | Document<PostAssetSchema>) => {
const { source } = asset;
let { path } = asset;
const data: AssetData = {
Expand Down
27 changes: 27 additions & 0 deletions lib/plugins/generator/code.ts
Original file line number Diff line number Diff line change
@@ -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<any[]> {
return Promise.filter(this.model('Code').toArray(), (code: Document<CodeSchema>) => exists(code.source).tap(exist => {
if (!exist) return code.remove();
})).map((code: Document<CodeSchema>) => {
Comment on lines +13 to +15
Copy link
Preview

Copilot AI Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation checks file existence for every code file on each generation. Consider implementing a more efficient approach that only checks existence when files are modified or caches the results.

Suggested change
return Promise.filter(this.model('Code').toArray(), (code: Document<CodeSchema>) => exists(code.source).tap(exist => {
if (!exist) return code.remove();
})).map((code: Document<CodeSchema>) => {
const fileExistenceCache: Record<string, boolean> = {};
return Promise.filter(this.model('Code').toArray(), (code: Document<CodeSchema>) => {
if (!code.modified && fileExistenceCache[code.source] !== undefined) {
// Use cached result if file is not modified
return Promise.resolve(fileExistenceCache[code.source]);
}
// Check file existence and update cache
return exists(code.source).tap(exist => {
fileExistenceCache[code.source] = exist;
if (!exist) return code.remove();
});
}).map((code: Document<CodeSchema>) => {

Copilot uses AI. Check for mistakes.

const { path } = code;
const data: CodeData = {
modified: code.modified,
data: code.content
};

return { path, data };
});

Comment on lines +12 to +24
Copy link
Preview

Copilot AI Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned object structure is inconsistent with other generators. Consider adding a comment explaining why this generator returns a different structure or align it with the BaseGeneratorReturn interface.

Suggested change
function codeGenerator(this: Hexo): Promise<any[]> {
return Promise.filter(this.model('Code').toArray(), (code: Document<CodeSchema>) => exists(code.source).tap(exist => {
if (!exist) return code.remove();
})).map((code: Document<CodeSchema>) => {
const { path } = code;
const data: CodeData = {
modified: code.modified,
data: code.content
};
return { path, data };
});
interface BaseGeneratorReturn {
path: string;
data: {
modified: boolean;
data: string;
};
}
function codeGenerator(this: Hexo): Promise<BaseGeneratorReturn[]> {
return Promise.filter(this.model('Code').toArray(), (code: Document<CodeSchema>) => exists(code.source).tap(exist => {
if (!exist) return code.remove();
})).map((code: Document<CodeSchema>) => {
const { path } = code;
const data: BaseGeneratorReturn['data'] = {
modified: code.modified,
data: code.content
};
return { path, data };
});

Copilot uses AI. Check for mistakes.

}

export = codeGenerator;
1 change: 1 addition & 0 deletions lib/plugins/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
3 changes: 3 additions & 0 deletions lib/plugins/processor/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions lib/plugins/processor/code.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
}
};
};
1 change: 1 addition & 0 deletions lib/plugins/processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export = (ctx: Hexo) => {
processor.register(obj.pattern, obj.process);
}

register('code');
register('asset');
register('data');
register('post');
Expand Down
4 changes: 2 additions & 2 deletions lib/plugins/tag/include_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
110 changes: 110 additions & 0 deletions test/scripts/generators/code.ts
Original file line number Diff line number Diff line change
@@ -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<typeof codeGenerator>
type CodeReturn = ReturnType<typeof codeGenerator>

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)
]);
});
});
59 changes: 59 additions & 0 deletions test/scripts/models/code.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading