diff --git a/lib/hexo/index.ts b/lib/hexo/index.ts index 6a8f695ad1..aeaab80087 100644 --- a/lib/hexo/index.ts +++ b/lib/hexo/index.ts @@ -40,6 +40,7 @@ import type Box from '../box'; import type { BaseGeneratorReturn, FilterOptions, LocalsType, NodeJSLikeCallback, SiteLocals } from '../types'; import type { AddSchemaTypeOptions } from 'warehouse/dist/types'; import type Schema from 'warehouse/dist/schema'; +import BinaryRelationIndex from '../models/binary_relation_index'; const libDir = dirname(__dirname); const dbVersion = 1; @@ -300,6 +301,10 @@ class Hexo extends EventEmitter { static lib_dir: string; static core_dir: string; static version: string; + public _binaryRelationIndex: { + post_tag: BinaryRelationIndex<'post_id', 'tag_id'>; + post_category: BinaryRelationIndex<'post_id', 'category_id'>; + }; constructor(base = process.cwd(), args: Args = {}) { super(); @@ -378,6 +383,10 @@ class Hexo extends EventEmitter { this.theme = new Theme(this); this.locals = new Locals(); this._bindLocals(); + this._binaryRelationIndex = { + post_tag: new BinaryRelationIndex<'post_id', 'tag_id'>('post_id', 'tag_id', 'PostTag', this), + post_category: new BinaryRelationIndex<'post_id', 'category_id'>('post_id', 'category_id', 'PostCategory', this) + }; } _bindLocals(): void { @@ -536,6 +545,8 @@ class Hexo extends EventEmitter { */ load(callback?: NodeJSLikeCallback): Promise { return loadDatabase(this).then(() => { + this._binaryRelationIndex.post_tag.load(); + this._binaryRelationIndex.post_category.load(); this.log.info('Start processing'); return Promise.all([ @@ -694,6 +705,9 @@ class Hexo extends EventEmitter { this.emit('generateBefore'); // Run before_generate filters + // https://github.com/hexojs/hexo/issues/5287 + // locals should be invalidated before before_generate filters because tags may use locals + this.locals.invalidate(); return this.execFilter('before_generate', null, { context: this }) .then(() => this._routerRefresh(this._runGenerators(), useCache)).then(() => { this.emit('generateAfter'); diff --git a/lib/hexo/post.ts b/lib/hexo/post.ts index d05a37af2d..c7706a667b 100644 --- a/lib/hexo/post.ts +++ b/lib/hexo/post.ts @@ -80,112 +80,174 @@ class PostRenderEscape { let swig_tag_name_end = false; let swig_tag_name = ''; let swig_full_tag_start_buffer = ''; + // current we just consider one level of string quote + let swig_string_quote = ''; const { length } = str; - for (let idx = 0; idx < length; idx++) { - const char = str[idx]; - const next_char = str[idx + 1]; + let idx = 0; - if (state === STATE_PLAINTEXT) { // From plain text to swig - if (char === '{') { - // check if it is a complete tag {{ }} - if (next_char === '{') { - state = STATE_SWIG_VAR; - idx++; - } else if (next_char === '#') { - state = STATE_SWIG_COMMENT; - idx++; - } else if (next_char === '%') { - state = STATE_SWIG_TAG; - idx++; - swig_tag_name = ''; - swig_full_tag_start_buffer = ''; - swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag - swig_tag_name_end = false; + // for backtracking + const swig_start_idx = { + [STATE_SWIG_VAR]: 0, + [STATE_SWIG_COMMENT]: 0, + [STATE_SWIG_TAG]: 0, + [STATE_SWIG_FULL_TAG]: 0 + }; + + while (idx < length) { + while (idx < length) { + const char = str[idx]; + const next_char = str[idx + 1]; + + if (state === STATE_PLAINTEXT) { // From plain text to swig + if (char === '{') { + // check if it is a complete tag {{ }} + if (next_char === '{') { + state = STATE_SWIG_VAR; + idx++; + swig_start_idx[state] = idx; + } else if (next_char === '#') { + state = STATE_SWIG_COMMENT; + idx++; + swig_start_idx[state] = idx; + } else if (next_char === '%') { + state = STATE_SWIG_TAG; + idx++; + swig_tag_name = ''; + swig_full_tag_start_buffer = ''; + swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag + swig_tag_name_end = false; + swig_start_idx[state] = idx; + } else { + output += char; + } } else { output += char; } - } else { - output += char; - } - } else if (state === STATE_SWIG_TAG) { - if (char === '%' && next_char === '}') { // From swig back to plain text - idx++; - if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) { - state = STATE_SWIG_FULL_TAG; - } else { + } else if (state === STATE_SWIG_TAG) { + if (char === '"' || char === '\'') { + if (swig_string_quote === '') { + swig_string_quote = char; + } else if (swig_string_quote === char) { + swig_string_quote = ''; + } + } + // {% } or {% % + if (((char !== '%' && next_char === '}') || (char === '%' && next_char !== '}')) && swig_string_quote === '') { + // From swig back to plain text swig_tag_name = ''; state = STATE_PLAINTEXT; - output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`); - } - - buffer = ''; - } else { - buffer = buffer + char; - swig_full_tag_start_buffer = swig_full_tag_start_buffer + char; - - if (isNonWhiteSpaceChar(char)) { - if (!swig_tag_name_begin && !swig_tag_name_end) { - swig_tag_name_begin = true; + output += `{%${buffer}${char}`; + buffer = ''; + } else if (char === '%' && next_char === '}' && swig_string_quote === '') { // From swig back to plain text + idx++; + if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) { + state = STATE_SWIG_FULL_TAG; + swig_start_idx[state] = idx; + } else { + swig_tag_name = ''; + state = STATE_PLAINTEXT; + output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`); } - if (swig_tag_name_begin) { - swig_tag_name += char; - } + buffer = ''; } else { - if (swig_tag_name_begin === true) { - swig_tag_name_begin = false; - swig_tag_name_end = true; + buffer = buffer + char; + swig_full_tag_start_buffer = swig_full_tag_start_buffer + char; + + if (isNonWhiteSpaceChar(char)) { + if (!swig_tag_name_begin && !swig_tag_name_end) { + swig_tag_name_begin = true; + } + + if (swig_tag_name_begin) { + swig_tag_name += char; + } + } else { + if (swig_tag_name_begin === true) { + swig_tag_name_begin = false; + swig_tag_name_end = true; + } } } - } - } else if (state === STATE_SWIG_VAR) { - if (char === '}' && next_char === '}') { - idx++; - state = STATE_PLAINTEXT; - output += PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`); - buffer = ''; - } else { - buffer = buffer + char; - } - } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text - if (char === '#' && next_char === '}') { - idx++; - state = STATE_PLAINTEXT; - buffer = ''; - } - } else if (state === STATE_SWIG_FULL_TAG) { - if (char === '{' && next_char === '%') { - let swig_full_tag_end_buffer = ''; - - let _idx = idx + 2; - for (; _idx < length; _idx++) { - const _char = str[_idx]; - const _next_char = str[_idx + 1]; - - if (_char === '%' && _next_char === '}') { - _idx++; - break; + } else if (state === STATE_SWIG_VAR) { + if (char === '"' || char === '\'') { + if (swig_string_quote === '') { + swig_string_quote = char; + } else if (swig_string_quote === char) { + swig_string_quote = ''; } - - swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char; } - - if (swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) { + // {{ } + if (char === '}' && next_char !== '}' && swig_string_quote === '') { + // From swig back to plain text + state = STATE_PLAINTEXT; + output += `{{${buffer}${char}`; + buffer = ''; + } else if (char === '}' && next_char === '}' && swig_string_quote === '') { + idx++; + state = STATE_PLAINTEXT; + output += PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`); + buffer = ''; + } else { + buffer = buffer + char; + } + } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text + if (char === '#' && next_char === '}') { + idx++; state = STATE_PLAINTEXT; - output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`); - idx = _idx; - swig_full_tag_start_buffer = ''; - swig_full_tag_end_buffer = ''; buffer = ''; + } + } else if (state === STATE_SWIG_FULL_TAG) { + if (char === '{' && next_char === '%') { + let swig_full_tag_end_buffer = ''; + let swig_full_tag_found = false; + + let _idx = idx + 2; + for (; _idx < length; _idx++) { + const _char = str[_idx]; + const _next_char = str[_idx + 1]; + + if (_char === '%' && _next_char === '}') { + _idx++; + swig_full_tag_found = true; + break; + } + + swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char; + } + + if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) { + state = STATE_PLAINTEXT; + output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`); + idx = _idx; + swig_full_tag_start_buffer = ''; + swig_full_tag_end_buffer = ''; + buffer = ''; + } else { + buffer += char; + } } else { buffer += char; } - } else { - buffer += char; } + idx++; + } + if (state === STATE_PLAINTEXT) { + break; + } + // If the swig tag is not closed, then it is a plain text, we need to backtrack + idx = swig_start_idx[state]; + buffer = ''; + swig_string_quote = ''; + if (state === STATE_SWIG_FULL_TAG) { + output += `{%${swig_full_tag_start_buffer}%`; + } else { + output += '{'; } + swig_full_tag_start_buffer = ''; + state = STATE_PLAINTEXT; } return output; diff --git a/lib/models/binary_relation_index.ts b/lib/models/binary_relation_index.ts new file mode 100644 index 0000000000..b9da452271 --- /dev/null +++ b/lib/models/binary_relation_index.ts @@ -0,0 +1,100 @@ +import type Hexo from '../hexo'; + +type BinaryRelationType = { + [key in K]: PropertyKey; +} & { + [key in V]: PropertyKey; +}; + +class BinaryRelationIndex { + keyIndex: Map> = new Map(); + valueIndex: Map> = new Map(); + key: K; + value: V; + ctx: Hexo; + schemaName: string; + + constructor(key: K, value: V, schemaName: string, ctx: Hexo) { + this.key = key; + this.value = value; + this.schemaName = schemaName; + this.ctx = ctx; + } + + load() { + this.keyIndex.clear(); + this.valueIndex.clear(); + const raw = this.ctx.model(this.schemaName).data; + for (const _id in raw) { + this.saveHook(raw[_id]); + } + } + + saveHook(data: BinaryRelationType & { _id: PropertyKey }) { + const _id = data._id; + const key = data[this.key]; + const value = data[this.value]; + if (!this.keyIndex.has(key)) { + this.keyIndex.set(key, new Set()); + } + this.keyIndex.get(key).add(_id); + + if (!this.valueIndex.has(value)) { + this.valueIndex.set(value, new Set()); + } + this.valueIndex.get(value).add(_id); + } + + removeHook(data: BinaryRelationType & { _id: PropertyKey }) { + const _id = data._id; + const key = data[this.key]; + const value = data[this.value]; + this.keyIndex.get(key)?.delete(_id); + if (this.keyIndex.get(key)?.size === 0) { + this.keyIndex.delete(key); + } + this.valueIndex.get(value)?.delete(_id); + if (this.valueIndex.get(value)?.size === 0) { + this.valueIndex.delete(value); + } + } + + findById(_id: PropertyKey) { + const raw = this.ctx.model(this.schemaName).findById(_id, { lean: true }); + if (!raw) return; + return { ...raw }; + } + + find(query: Partial>) { + const key = query[this.key]; + const value = query[this.value]; + + if (key && value) { + const ids = this.keyIndex.get(key); + if (!ids) return []; + return Array.from(ids) + .map(_id => this.findById(_id)) + .filter(record => record?.[this.value] === value); + } + + if (key) { + const ids = this.keyIndex.get(key); + if (!ids) return []; + return Array.from(ids).map(_id => this.findById(_id)); + } + + if (value) { + const ids = this.valueIndex.get(value); + if (!ids) return []; + return Array.from(ids).map(_id => this.findById(_id)); + } + + return []; + } + + findOne(query: Partial>) { + return this.find(query)[0]; + } +} + +export default BinaryRelationIndex; diff --git a/lib/models/category.ts b/lib/models/category.ts index 6897ddf157..bc6338543a 100644 --- a/lib/models/category.ts +++ b/lib/models/category.ts @@ -42,9 +42,9 @@ export = (ctx: Hexo) => { }); Category.virtual('posts').get(function() { - const PostCategory = ctx.model('PostCategory'); + const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; - const ids = PostCategory.find({category_id: this._id}).map(item => item.post_id); + const ids = ReadOnlyPostCategory.find({category_id: this._id}).map(item => item.post_id); return ctx.locals.get('posts').find({ _id: {$in: ids} @@ -52,9 +52,9 @@ export = (ctx: Hexo) => { }); Category.virtual('length').get(function() { - const PostCategory = ctx.model('PostCategory'); + const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; - return PostCategory.find({category_id: this._id}).length; + return ReadOnlyPostCategory.find({category_id: this._id}).length; }); // Check whether a category exists diff --git a/lib/models/post.ts b/lib/models/post.ts index 63ae5e05e4..fcbc5df4dc 100644 --- a/lib/models/post.ts +++ b/lib/models/post.ts @@ -65,10 +65,10 @@ export = (ctx: Hexo) => { Post.virtual('tags').get(function() { return tagsGetterCache.apply(this._id, () => { - const PostTag = ctx.model('PostTag'); + const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; const Tag = ctx.model('Tag'); - const ids = PostTag.find({post_id: this._id}, {lean: true}).map(item => item.tag_id); + const ids = ReadOnlyPostTag.find({post_id: this._id}).map(item => item.tag_id); return Tag.find({_id: {$in: ids}}); }); @@ -88,10 +88,11 @@ export = (ctx: Hexo) => { tagsGetterCache.flush(); tags = removeEmptyTag(tags); + const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; const PostTag = ctx.model('PostTag'); const Tag = ctx.model('Tag'); const id = this._id; - const existed = PostTag.find({post_id: id}, {lean: true}).map(pickID); + const existed = ReadOnlyPostTag.find({post_id: id}).map(pickID); return Promise.map(tags, tag => { // Find the tag by name @@ -108,7 +109,7 @@ export = (ctx: Hexo) => { }); }).map(tag => { // Find the reference - const ref = PostTag.findOne({post_id: id, tag_id: tag._id}, {lean: true}); + const ref = ReadOnlyPostTag.findOne({post_id: id, tag_id: tag._id}); if (ref) return ref; // Insert the reference if not exist @@ -124,10 +125,10 @@ export = (ctx: Hexo) => { }); Post.virtual('categories').get(function() { - const PostCategory = ctx.model('PostCategory'); + const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; const Category = ctx.model('Category'); - const ids = PostCategory.find({post_id: this._id}, {lean: true}).map(item => item.category_id); + const ids = ReadOnlyPostCategory.find({post_id: this._id}).map(item => item.category_id); return Category.find({_id: {$in: ids}}); }); @@ -143,11 +144,12 @@ export = (ctx: Hexo) => { return Array.isArray(cat) ? removeEmptyTag(cat) : `${cat}`; }); + const ReadOnlyPostCategory = ctx._binaryRelationIndex.post_category; const PostCategory = ctx.model('PostCategory'); const Category = ctx.model('Category'); const id = this._id; const allIds: string[] = []; - const existed = PostCategory.find({post_id: id}, {lean: true}).map(pickID); + const existed = ReadOnlyPostCategory.find({post_id: id}).map(pickID); const hasHierarchy = cats.filter(Array.isArray).length > 0; // Add a hierarchy of categories @@ -193,7 +195,7 @@ export = (ctx: Hexo) => { return (hasHierarchy ? Promise.each(cats, addHierarchy) : Promise.resolve(addHierarchy(cats as string[])) ).then(() => allIds).map(catId => { // Find the reference - const ref: PostCategorySchema = PostCategory.findOne({post_id: id, category_id: catId}, {lean: true}); + const ref: PostCategorySchema = ReadOnlyPostCategory.findOne({post_id: id, category_id: catId}); if (ref) return ref; // Insert the reference if not exist diff --git a/lib/models/post_category.ts b/lib/models/post_category.ts index dd793b5bb0..087213d044 100644 --- a/lib/models/post_category.ts +++ b/lib/models/post_category.ts @@ -8,5 +8,20 @@ export = (ctx: Hexo) => { category_id: {type: warehouse.Schema.Types.CUID, ref: 'Category'} }); + PostCategory.pre('save', data => { + ctx._binaryRelationIndex.post_category.removeHook(data); + return data; + }); + + PostCategory.post('save', data => { + ctx._binaryRelationIndex.post_category.saveHook(data); + return data; + }); + + PostCategory.pre('remove', data => { + ctx._binaryRelationIndex.post_category.removeHook(data); + return data; + }); + return PostCategory; }; diff --git a/lib/models/post_tag.ts b/lib/models/post_tag.ts index dae82954ac..af838f885c 100644 --- a/lib/models/post_tag.ts +++ b/lib/models/post_tag.ts @@ -8,5 +8,20 @@ export = (ctx: Hexo) => { tag_id: {type: warehouse.Schema.Types.CUID, ref: 'Tag'} }); + PostTag.pre('save', data => { + ctx._binaryRelationIndex.post_tag.removeHook(data); + return data; + }); + + PostTag.post('save', data => { + ctx._binaryRelationIndex.post_tag.saveHook(data); + return data; + }); + + PostTag.pre('remove', data => { + ctx._binaryRelationIndex.post_tag.removeHook(data); + return data; + }); + return PostTag; }; diff --git a/lib/models/tag.ts b/lib/models/tag.ts index 619c5a658f..006a0260ce 100644 --- a/lib/models/tag.ts +++ b/lib/models/tag.ts @@ -33,9 +33,9 @@ export = (ctx: Hexo) => { }); Tag.virtual('posts').get(function() { - const PostTag = ctx.model('PostTag'); + const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; - const ids = PostTag.find({tag_id: this._id}).map(item => item.post_id); + const ids = ReadOnlyPostTag.find({tag_id: this._id}).map(item => item.post_id); return ctx.locals.get('posts').find({ _id: {$in: ids} @@ -45,9 +45,9 @@ export = (ctx: Hexo) => { Tag.virtual('length').get(function() { // Note: this.posts.length is also working // But it's slow because `find` has to iterate over all posts - const PostTag = ctx.model('PostTag'); + const ReadOnlyPostTag = ctx._binaryRelationIndex.post_tag; - return PostTag.find({tag_id: this._id}).length; + return ReadOnlyPostTag.find({tag_id: this._id}).length; }); // Check whether a tag exists diff --git a/test/scripts/extend/tag.ts b/test/scripts/extend/tag.ts index 0407558a86..1e3ad33c9e 100644 --- a/test/scripts/extend/tag.ts +++ b/test/scripts/extend/tag.ts @@ -1,10 +1,53 @@ +import { join } from 'path'; import Tag from '../../../lib/extend/tag'; import chai from 'chai'; +import Hexo from '../../../lib/hexo'; +import defaultConfig from '../../../lib/hexo/default_config'; +import posts from '../../../lib/plugins/processor/post'; +import Filter from '../../../lib/extend/filter'; +import renderPostFilter from '../../../lib/plugins/filter/before_generate/render_post'; +import { mkdirs, rmdir, writeFile } from 'hexo-fs'; +// @ts-ignore +import Promise from 'bluebird'; const should = chai.should(); +type PostParams = Parameters['process']> +type PostReturn = ReturnType['process']> + describe('Tag', () => { const tag = new Tag(); + const baseDir = join(__dirname, 'post_test'); + const hexo = new Hexo(baseDir); + const post = posts(hexo); + const process: (...args: PostParams) => Promise = Promise.method(post.process.bind(hexo)); + const { source } = hexo; + const { File } = source; + + function newFile(options) { + const { path } = options; + + options.path = (options.published ? '_posts' : '_drafts') + '/' + path; + options.source = join(source.base, options.path); + + options.params = { + published: options.published, + path, + renderable: options.renderable + }; + + return new File(options); + } + + before(async () => { + await mkdirs(baseDir); + hexo.init(); + }); + + beforeEach(() => { hexo.config = Object.assign({}, defaultConfig); }); + + after(() => rmdir(baseDir)); + it('register()', async () => { const tag = new Tag(); @@ -180,4 +223,75 @@ describe('Tag', () => { spy.should.eql(true); }); }); + + it('tag should get right locals', async () => { + let count = 0; + hexo.extend.filter = new Filter(); + hexo.extend.tag = new Tag(); + hexo.extend.tag.register('series', () => { + count = hexo.locals.get('posts').length; + return ''; + }, {ends: false}); + hexo.extend.filter.register('before_generate', renderPostFilter.bind(hexo)); + + const body1 = [ + 'title: "test1"', + 'date: 2023-09-03 16:59:42', + 'tags: foo', + '---', + '{% series %}' + ].join('\n'); + + const file = newFile({ + path: 'test1.html', + published: true, + type: 'create', + renderable: true + }); + + const body2 = [ + '---', + 'title: test2', + 'date: 2023-09-03 16:59:46', + 'tags: foo', + '---' + ]; + + const file2 = newFile({ + path: 'test2.html', + published: true, + type: 'create', + renderable: true + }); + + const body3 = [ + 'title: test3', + 'date: 2023-09-03 16:59:49', + 'tags: foo', + '---' + ]; + + const file3 = newFile({ + path: 'test3.html', + published: true, + type: 'create', + renderable: true + }); + + await Promise.all([ + writeFile(file.source, body1), + writeFile(file2.source, body2), + writeFile(file3.source, body3) + ]); + + await Promise.all([ + process(file), + process(file2), + process(file3) + ]); + + await hexo._generate({ cache: false }); + + count.should.eql(3); + }); }); diff --git a/test/scripts/hexo/post.ts b/test/scripts/hexo/post.ts index 79a4328ce7..f2987f6f12 100644 --- a/test/scripts/hexo/post.ts +++ b/test/scripts/hexo/post.ts @@ -1423,6 +1423,98 @@ describe('Post', () => { data.content.should.not.contains('`'); // ` }); + it('render() - should support quotes in tags', async () => { + let content = '{{ "{{ }" }}'; + let data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.eql('{{ }'); + + content = '{% blockquote "{% }" %}test{% endblockquote %}'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.eql('

test

\n
'); + }); + + it('render() - dont escape incomplete tags with complete tags', async () => { + // lost one character + let content = '{{ 1 }} \n `{% "%}" }` 22222'; + let data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{% "%}" }'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + content = '{{ 1 }} \n `{% "%}" %` 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{% "%}" %'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + content = '{{ 1 }} \n `{# }` 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{# }'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + content = '{{ 1 }} \n `{{ "}}" }` 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{{ "}}" }'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + content = '{{ 1 }} \n `{{ %}` 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{{ %}'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + // lost two characters + content = '{{ 1 }} \n `{#` \n 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{#'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + content = '{{ 1 }} \n `{%` \n 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('{%'); + data.content.should.contains('1'); + data.content.should.contains('22222'); + + content = '{{ 1 }} \n `{{ ` 22222'; + data = await post.render('', { + content, + engine: 'markdown' + }); + data.content.should.contains('1'); + data.content.should.contains('{{ '); + data.content.should.contains('22222'); + }); + it('render() - incomplete tags throw error', async () => { const content = 'nunjucks should throw {# } error'; diff --git a/test/scripts/models/category.ts b/test/scripts/models/category.ts index c0b0009b10..f5c75006a5 100644 --- a/test/scripts/models/category.ts +++ b/test/scripts/models/category.ts @@ -6,6 +6,7 @@ describe('Category', () => { const hexo = new Hexo(); const Category = hexo.model('Category'); const Post = hexo.model('Post'); + const ReadOnlyPostCategory = hexo._binaryRelationIndex.post_category; const PostCategory = hexo.model('PostCategory'); before(() => hexo.init()); @@ -283,6 +284,7 @@ describe('Category', () => { await Category.removeById(cat._id!); PostCategory.find({category_id: cat._id}).should.have.lengthOf(0); + ReadOnlyPostCategory.find({category_id: cat._id}).should.have.lengthOf(0); await Promise.all(posts.map(post => post.remove())); }); diff --git a/test/scripts/models/post.ts b/test/scripts/models/post.ts index 6c02b54246..93c26c52c5 100644 --- a/test/scripts/models/post.ts +++ b/test/scripts/models/post.ts @@ -10,6 +10,8 @@ describe('Post', () => { const Post = hexo.model('Post'); const Tag = hexo.model('Tag'); const Category = hexo.model('Category'); + const ReadOnlyPostTag = hexo._binaryRelationIndex.post_tag; + const ReadOnlyPostCategory = hexo._binaryRelationIndex.post_category; const PostTag = hexo.model('PostTag'); const PostCategory = hexo.model('PostCategory'); const Asset = hexo.model('Asset'); @@ -426,6 +428,7 @@ describe('Post', () => { }).then(post => post.setTags(['foo', 'bar', 'baz']) .thenReturn(Post.findById(post._id))).then(post => Post.removeById(post._id)).then(post => { PostTag.find({post_id: post._id}).should.have.lengthOf(0); + ReadOnlyPostTag.find({post_id: post._id}).should.have.lengthOf(0); Tag.findOne({name: 'foo'}).posts.should.have.lengthOf(0); Tag.findOne({name: 'bar'}).posts.should.have.lengthOf(0); Tag.findOne({name: 'baz'}).posts.should.have.lengthOf(0); @@ -437,6 +440,7 @@ describe('Post', () => { }).then(post => post.setCategories(['foo', 'bar', 'baz']) .thenReturn(Post.findById(post._id))).then(post => Post.removeById(post._id)).then(post => { PostCategory.find({post_id: post._id}).should.have.lengthOf(0); + ReadOnlyPostCategory.find({post_id: post._id}).should.have.lengthOf(0); Category.findOne({name: 'foo'}).posts.should.have.lengthOf(0); Category.findOne({name: 'bar'}).posts.should.have.lengthOf(0); Category.findOne({name: 'baz'}).posts.should.have.lengthOf(0); diff --git a/test/scripts/models/tag.ts b/test/scripts/models/tag.ts index 94108d6521..a80ec85383 100644 --- a/test/scripts/models/tag.ts +++ b/test/scripts/models/tag.ts @@ -7,6 +7,7 @@ describe('Tag', () => { const Tag = hexo.model('Tag'); const Post = hexo.model('Post'); const PostTag = hexo.model('PostTag'); + const ReadOnlyPostTag = hexo._binaryRelationIndex.post_tag; before(() => hexo.init()); @@ -244,6 +245,7 @@ describe('Tag', () => { await Tag.removeById(tag._id!); PostTag.find({tag_id: tag._id}).should.have.lengthOf(0); + ReadOnlyPostTag.find({tag_id: tag._id}).should.have.lengthOf(0); await Promise.all(posts.map(post => Post.removeById(post._id))); });