const fs = require('fs-extra'), path = require('path'), { red } = require('kleur'), crypto = require('crypto'), frontmatter = require('front-matter') const sass = require('node-sass') const caniuseDb = require('caniuse-db/data.json') const config = require('../../config') // Reade all files in a directory const getFilesInDir = (directoryPath, withPath, exclude = null) => { try { let directoryFilenames = fs.readdirSync(directoryPath) directoryFilenames.sort((a, b) => { a = a.toLowerCase() b = b.toLowerCase() if (a < b) return -1 if (a > b) return 1 return 0 }) if (withPath) { // a hacky way to do conditional array.map return directoryFilenames.reduce((fileNames, fileName) => { if (exclude == null || !exclude.some(toExclude => fileName === toExclude)) fileNames.push(`${directoryPath}/${fileName}`) return fileNames }, []) } return directoryFilenames.filter(v => v !== 'README.md') } catch (err) { console.log(`${red('ERROR!')} During snippet loading: ${err}`) process.exit(1) } } // Creates a hash for a value using the SHA-256 algorithm. const hashData = val => crypto .createHash('sha256') .update(val) .digest('hex') // Gets the code blocks for a snippet file. const getCodeBlocks = str => { const regex = /```[.\S\s]*?```/g let results = [] let m = null while ((m = regex.exec(str)) !== null) { if (m.index === regex.lastIndex) regex.lastIndex += 1 m.forEach((match, groupIndex) => { results.push(match) }) } const replacer = new RegExp(`\`\`\`${config.language}([\\s\\S]*?)\`\`\``, 'g') const secondReplacer = new RegExp(`\`\`\`${config.secondLanguage}([\\s\\S]*?)\`\`\``, 'g') const optionalReplacer = new RegExp(`\`\`\`${config.optionalLanguage}([\\s\\S]*?)\`\`\``, 'g') results = results.map(v => v .replace(replacer, '$1') .replace(secondReplacer, '$1') .replace(optionalReplacer, '$1') .trim() ) if (results.length > 2) return { html: results[0], css: results[1], js: results[2] } return { html: results[0], css: results[1], js: '' } } // Gets the textual content for a snippet file. const getTextualContent = str => { const regex = /([\s\S]*?)```/g const results = [] let m = null while ((m = regex.exec(str)) !== null) { if (m.index === regex.lastIndex) regex.lastIndex += 1 m.forEach((match, groupIndex) => { results.push(match) }) } return results[1].replace(/\r\n/g, '\n') } // Gets the explanation for a snippet file. const getExplanation = str => { const regex = /####\s*Explanation([\s\S]*)####/g const results = [] let m = null while ((m = regex.exec(str)) !== null) { if (m.index === regex.lastIndex) regex.lastIndex += 1 m.forEach((match, groupIndex) => { results.push(match) }) } // console.log(results); return results[1].replace(/\r\n/g, '\n') } // Gets the browser support for a snippet file. const getBrowserSupport = str => { const regex = /####\s*Browser [s|S]upport([\s\S]*)/g const results = [] let m = null while ((m = regex.exec(str)) !== null) { if (m.index === regex.lastIndex) regex.lastIndex += 1 m.forEach((match, groupIndex) => { results.push(match) }) } let browserSupportText = results[1].replace(/\r\n/g, '\n') const supportPercentage = ( browserSupportText.match(/https?:\/\/caniuse\.com\/#feat=.*/g) || [] ).map(feat => { const featData = caniuseDb.data[feat.match(/#feat=(.*)/)[1]] // caniuse doesn't count "untracked" users, which makes the overall share appear much lower // than it probably is. Most of these untracked browsers probably support these features. // Currently it's around 5.3% untracked, so we'll use 4% as probably supporting the feature. // Also the npm package appears to be show higher usage % than the main website, this shows // about 0.2% lower than the main website when selecting "tracked users" (as of Feb 2019). const UNTRACKED_PERCENT = 4 const usage = featData ? Number(featData.usage_perc_y + featData.usage_perc_a) + UNTRACKED_PERCENT : 100 return Math.min(100, usage) }) return { text: browserSupportText, supportPercentage: Math.min(...supportPercentage, 100) } } // Synchronously read all snippets and sort them as necessary (case-insensitive) const readSnippets = snippetsPath => { const snippetFilenames = getFilesInDir(snippetsPath, false) let snippets = {} try { for (let snippet of snippetFilenames) { let data = frontmatter(fs.readFileSync(path.join(snippetsPath, snippet), 'utf8')) snippets[snippet] = { id: snippet.slice(0, -3), title: data.attributes.title, type: 'snippet', attributes: { fileName: snippet, text: getTextualContent(data.body), explanation: getExplanation(data.body), browserSupport: getBrowserSupport(data.body), codeBlocks: getCodeBlocks(data.body), tags: data.attributes.tags.split(',').map(t => t.trim()) }, meta: { hash: hashData(data.body) } } snippets[snippet].attributes.codeBlocks.scopedCss = sass .renderSync({ data: `[data-scope="${snippets[snippet].id}"] { ${snippets[snippet].attributes.codeBlocks.css} }` }) .css.toString() } } catch (err) { console.log(`${red('ERROR!')} During snippet loading: ${err}`) process.exit(1) } return snippets } module.exports = { getFilesInDir, hashData, getCodeBlocks, getTextualContent, getExplanation, getBrowserSupport, readSnippets }