const glob = require('glob'); const path = require('path'); const fs = require('fs'); const cheerio = require('cheerio'); const sass = require('sass'); const args = process.argv.slice(2); let partten = "**/*.htmv"; if (args[1]) { partten = args[1]; } let dirPath = args[0] || './src'; /** * * @param {String} dirPath * @param {String} partten */ function dealVueStyleNode(dirPath, partten) { const files = glob.globSync(partten, { cwd: dirPath }); files.forEach(file => dealVueStyleNodeSingleFile(path.resolve(dirPath, file))); } const rewriter = vueStyleRewriter(); function dealVueStyleNodeSingleFile(file) { const html = fs.readFileSync(file, 'utf-8'); let styleIndex = 0; const rewritedHtml = rewriter(html, (style) => { // 使用 cheerio 加载 HTML 文档 // 第三个参数表示不加载到document中 const $ = cheerio.load(style, { lowerCaseTags: false, xmlMode: true, normalizeWhitespace: false, decodeEntities: false, }, false); $('style').each((index, styleEl) => { styleEl = $(styleEl); const lang = styleEl.attr('lang'); if (!lang || lang.indexOf('scss') == 0) { const scssString = styleEl.text(); try { const result = parseScss(scssString, file); styleEl.text(` ${result.css} `); console.log(`file: ${file} style-node: ${styleIndex} deal style success.`); } catch (e) { console.warn(`file: ${file} style-node: ${styleIndex} deal style error.`, e); } if (lang && lang.indexOf('scoped') >= 0) { styleEl.attr('scoped', true); } styleEl.attr('lang', 'css'); } }); styleIndex ++; return $.html(); }); if (styleIndex > 0) { // // 将修改后的 HTML 文档写入文件 fs.writeFileSync(file, rewritedHtml); } } function parseScss(scssString, file) { const replaceTagSelector = 'tag::replace'; const result = sass.compileString(`${replaceTagSelector} {${scssString}}`, { url: file }); result.css = result.css // + "\n\t" + result.css .replace(/tag::replace([\.:\[])/g, '&$1') .replace(/tag::replace[\s]*[\{]/g, '& {') .replace(/tag::replace[\s]*/g, ''); return result; } dealVueStyleNode(dirPath, partten); function vueStyleRewriter() { // 从 node_modules\vue-template-compiler\build.js 中 复用解析代码 /** * Always return false. */ const no = (a, b, c) => false; // HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3 // Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content const isNonPhrasingTag = makeMap('address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' + 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' + 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' + 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' + 'title,tr,track'); /** * Make a map and return a function for checking if a key * is in that map. */ function makeMap(str, expectsLowerCase) { const map = Object.create(null); const list = str.split(','); for (let i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val]; } const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; /** * Not type-checking this file because it's mostly vendor code. */ // Regular Expressions for parsing tags and attributes const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`; const qnameCapture = `((?:${ncname}\\:)?${ncname})`; const startTagOpen = new RegExp(`^<${qnameCapture}`); const startTagClose = /^\s*(\/?)>/; const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); const doctype = /^]+>/i; // #7298: escape - to avoid being passed as HTML comment when inlined in page const comment = /^', '"': '"', '&': '&', ' ': '\n', ' ': '\t', ''': "'" }; const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g; const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g; // #5992 const isIgnoreNewlineTag = makeMap('pre,textarea', true); const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'; function decodeAttr(value, shouldDecodeNewlines) { const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr; return value.replace(re, match => decodingMap[match]); } function parseHTML(html, options) { const stack = []; const expectHTML = options.expectHTML; const isUnaryTag = options.isUnaryTag || no; const canBeLeftOpenTag = options.canBeLeftOpenTag || no; let index = 0; let last, lastTag; while (html) { last = html; // Make sure we're not in a plaintext content element like script/style if (!lastTag || !isPlainTextElement(lastTag)) { let textEnd = html.indexOf('<'); if (textEnd === 0) { // Comment: if (comment.test(html)) { const commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment && options.comment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } advance(commentEnd + 3); continue; } } // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment if (conditionalComment.test(html)) { const conditionalEnd = html.indexOf(']>'); if (conditionalEnd >= 0) { advance(conditionalEnd + 2); continue; } } // Doctype: const doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); continue; } // End tag: const endTagMatch = html.match(endTag); if (endTagMatch) { const curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue; } // Start tag: const startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue; } } let text, rest, next; if (textEnd >= 0) { rest = html.slice(textEnd); while (!endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest)) { // < in plain text, be forgiving and treat it as text next = rest.indexOf('<', 1); if (next < 0) break; textEnd += next; rest = html.slice(textEnd); } text = html.substring(0, textEnd); } if (textEnd < 0) { text = html; } if (text) { advance(text.length); } if (options.chars && text) { options.chars(text, index - text.length, index); } } else { let endTagLength = 0; const stackedTag = lastTag.toLowerCase(); const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(]*>)', 'i')); const rest = html.replace(reStackedTag, function (all, text, endTag) { endTagLength = endTag.length; if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { text = text .replace(//g, '$1') // #7298 .replace(//g, '$1'); } if (shouldIgnoreFirstNewline(stackedTag, text)) { text = text.slice(1); } if (options.chars) { options.chars(text); } return ''; }); index += html.length - rest.length; html = rest; parseEndTag(stackedTag, index - endTagLength, index); } if (html === last) { options.chars && options.chars(html); if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) { options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length }); } break; } } // Clean up any remaining tags parseEndTag(); function advance(n) { index += n; html = html.substring(n); } function parseStartTag() { const start = html.match(startTagOpen); if (start) { const match = { tagName: start[1], attrs: [], start: index }; advance(start[0].length); let end, attr; while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index; advance(attr[0].length); attr.end = index; match.attrs.push(attr); } if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match; } } } function handleStartTag(match) { const tagName = match.tagName; const unarySlash = match.unarySlash; if (expectHTML) { if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag); } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName); } } const unary = isUnaryTag(tagName) || !!unarySlash; const l = match.attrs.length; const attrs = new Array(l); for (let i = 0; i < l; i++) { const args = match.attrs[i]; const value = args[3] || args[4] || args[5] || ''; const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines; attrs[i] = { name: args[1], value: decodeAttr(value, shouldDecodeNewlines) }; if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { attrs[i].start = args.start + args[0].match(/^\s*/).length; attrs[i].end = args.end; } } if (!unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }); lastTag = tagName; } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end); } } function parseEndTag(tagName, start, end) { let pos, lowerCasedTagName; if (start == null) start = index; if (end == null) end = index; // Find the closest opened tag of the same type if (tagName) { lowerCasedTagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break; } } } else { // If no tag name is provided, clean shop pos = 0; } if (pos >= 0) { // Close all the open elements, up the stack for (let i = stack.length - 1; i >= pos; i--) { if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn) { options.warn(`tag <${stack[i].tag}> has no matching end tag.`, { start: stack[i].start, end: stack[i].end }); } if (options.end) { options.end(stack[i].tag, start, end); } } // Remove the open elements from the stack stack.length = pos; lastTag = pos && stack[pos - 1].tag; } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end); } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end); } if (options.end) { options.end(tagName, start, end); } } } } return function parseVUE(vueString, replaceStyle) { const items = []; let currentParent; parseHTML(vueString, { shouldKeepComment: true, start(tag, attrs, unary, start, end) { if (!unary) { currentParent = { tag, attrs, unary, start, end }; } else { currentParent = null; } if (currentParent != null && currentParent.tag == 'style') { } else { items.push(vueString.substring(start, end)); } }, end(tag, start, end) { if (currentParent != null && currentParent.tag == 'style') { const tagStr = vueString.substring(currentParent.start, end); items.push(replaceStyle(tagStr)); } else { items.push(vueString.substring(start, end)); } currentParent = null; }, chars(text, start, end) { if (currentParent != null && currentParent.tag == 'style') { } else { items.push(text); } }, comment(text, start, end) { // adding anything as a sibling to the root node is forbidden // comments should still be allowed, but ignored items.push(vueString.substring(start, end)); } }) return items.join("") } }