chameleon-linter
Version:
cml规范校验工具
208 lines (177 loc) • 6.21 kB
JavaScript
const path = require('path');
const htmllint = require('cml-htmllint');
const config = require('../config');
const whiteListConifg = require('../config/white-list');
const builtinComponents = require('../config/built-in-components');
const tagEmbedRules = require('../config/tag-embed-rules.json');
const fakeComps = require('../config/fakeComps');
const utils = require('../utils');
const linter = new htmllint.Linter();
const CML_METHODNAME_REGEX = /(?:c-bind:|c-catch:)(\w+)/;
const VUE_METHODNAME_REGEX = /(?:v-on:|v-once:|@)(\w+)/;
const VUE_PROP_REGEX = /(?:v-bind:|:)((?:\w|-)+)/;
const APP_ENTRANCE_FILENAME = 'app.cml';
/**
* Parse json ast and retrive the custumized component names.
* @param {Object} jsonAst
* @returns An array contains all the component names. ej. [{name: 'view', isOrigin: true}, {name:'picker', isOrigin: false}]
*/
function getCustimizedTags(jsonAst, {platform = '', templatePath = ''}) {
let result = []; let componentsObj = {};
if (jsonAst) {
let baseJson = jsonAst.base || {};
let platformJson = {};
if (platform) {
platformJson = jsonAst[platform] || {};
}
Object.assign(componentsObj, baseJson.usingComponents, platformJson.usingComponents);
}
result = Object.entries(componentsObj)
.map((infoPair) => {
let [name, basePath] = infoPair;
return {
name: utils.toDash(name),
isOrigin: !utils.isCmlComponent(path.resolve(config.getCurrentWorkspace(), templatePath), basePath)
}
});
return result;
}
/**
* Getting options for html linter.
* @params {Object}
* platformType: 'web'|'wx'|'weex' | '' // '' stands for single file component
* lang: 'cml'|'vue'
* custimizedTags: tags appeding by usingComponents.
*/
function getLintOptions(params) {
let options = {
'line-no-trailing-whitespace': false,
'line-end-style': false,
'attr-name-style': false,
'indent-width': false,
'class-style': 'none',
'template-req-lang': false,
'attr-quote-style': 'quoted',
'spec-char-escape': false,
'attr-req-value': false,
'tag-close': true // ignoring tag close detection for now, a better solution will be provide in the future.
};
options['template-lang-allows'] = ['cml', 'vue'];
options['template-lang'] = params.lang;
options['attr-bans'] = whiteListConifg.getForbiddenAttrsByLang(params.lang);
options['tag-only-allowed-names'] = whiteListConifg
.getAllowedTags()
.concat(params.custimizedTags
.map((tag) => {
return tag.name;
}));
// origin components that are referenced by usingComponents.
options['origin-tag-list'] = params.custimizedTags
.filter((tag) => {
return tag.isOrigin;
})
.map((tag) => {
return tag.name;
});
// options associated with directives.
if (params.lang === 'vue') {
options['directive-name-forbidden-regex'] = /^(c-(?:bind|catch)(?!\w))/;
} else {
options['directive-name-forbidden-regex'] = /^((?:v-(?:bind|on|once)(?!\w))|:|@)/;
options['text-ignore-regex'] = /{{(.*?)}}/;
}
options['cml-directives'] = whiteListConifg.getForbiddenAttrsByLang('vue');
options['cml-valid-value-regex'] = /^\s*{{.*}}\s*$/;
// built-in components configurations.
options['component-allow-attr'] = builtinComponents.getCml();
if (params.lang === 'cml') {
options['component-event-regex'] = CML_METHODNAME_REGEX;
options['component-prop-regex'] = false;
} else {
options['component-event-regex'] = VUE_METHODNAME_REGEX;
options['component-prop-regex'] = VUE_PROP_REGEX;
}
// built-in embed tags' rules
options['tag-embed-tags'] = tagEmbedRules;
// origin components rules
options['origin-tag-forbiden-directive-regex'] = new RegExp('^(' + whiteListConifg.getForbiddenAttrsByLang('vue').join('|') + ')');
return options;
}
/**
* A temporary solution for application entrance file app.cml
* @param {Object} options lint options
*/
function addFakeComp(options, comp) {
if (options['component-allow-attr']) {
options['component-allow-attr'][comp.name] = comp.allowAttrs;
}
if (options['tag-only-allowed-names']) {
options['tag-only-allowed-names'].push(comp.name);
}
}
function isAppEntranceFile(file) {
return file && path.basename(file) === APP_ENTRANCE_FILENAME;
}
/**
* @part {Object}
* line: line offset of template part.
* rawContent: the whole tempalte string.
* platformType:'web'|'wx'|'weex'
* params: {
* lang:'cml'
* }
* @jsonAst {Object} An ast of json part.
*/
module.exports = (part, jsonAst) => {
return new Promise(async (resolve, reject) => {
let ast = {}; let messages = []; let lintResults = []; let lintOptions = {}; let templateMatches = [];
part.params.lang || (part.params.lang = 'cml');
ast = linter.parser.parse(part.rawContent);
templateMatches = part.rawContent.match(/<\/?\s*?template/ig);
// eslint-disable-next-line no-magic-numbers
if (!templateMatches || templateMatches.length !== 2) {
return resolve({
start: part.line,
ast,
messages: [{
line: part.line + 1,
column: 0,
token: 'template',
msg: 'Each template can only have one group of template tags'
}]
});
}
lintOptions = getLintOptions({
...part.params,
platformType: part.platformType || 'cml',
...{custimizedTags: getCustimizedTags(jsonAst, {
templatePath: part.file,
platform: part.platformType
})}
});
// adding fake comps
const allowedFakeComps = ['component'];
if (isAppEntranceFile(part.file)) {
allowedFakeComps.push('app');
}
fakeComps && fakeComps.forEach((comp) => {
if (~allowedFakeComps.indexOf(comp.name)) {
addFakeComp(lintOptions, comp);
}
});
lintResults = await htmllint(part.rawContent, lintOptions);
lintResults && lintResults.forEach((item) => {
messages.push({
line: item.line,
column: item.column,
token: htmllint.messages.guessToken(item),
msg: htmllint.messages.renderIssue(item)
});
});
resolve({
start: part.line,
ast,
messages
});
});
};