Last active
April 17, 2020 12:39
-
-
Save botoxparty/3eaf29fbb8b69e52fd2f27d6d2e7dcd3 to your computer and use it in GitHub Desktop.
gatsby-remark-embed-gist filter-by-lines
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import querystring from "querystring"; | |
import cheerio from "cheerio"; | |
import rangeParser from "parse-numeric-range"; | |
import request from "request-promise"; | |
import visit from "async-unist-util-visit"; | |
// default base url | |
const baseUrl = "https://gist.github.com"; | |
/** | |
* @typedef {object} PluginOptions | |
* @property {string} username the default gist user. | |
* @property {boolean} includeDefaultCss a flag indicating the default css should be included | |
*/ | |
/** | |
* @typedef {object} GistQuery | |
* @property {string} file the file name. | |
* @property {string|Array<number>} highlights the numbers to be highlighted. | |
*/ | |
/** | |
* Validates the query object is valid. | |
* @param {GistQuery} query the query to be validated. | |
* @returns {boolean} true if the query is valid; false otherwise. | |
*/ | |
function isValid(query) { | |
if (query == null) return false; | |
if (query.file == null && query.highlights == null) return false; | |
// leaving this for future enhancements to the query object | |
return true; | |
} | |
/** | |
* Builds the query object. | |
* This methods looks for anything that is after ? or # in the gist: directive. | |
* ? is interpreted as a query string. | |
* # is interpreted as a filename. | |
* @param {string} value the value of the inlineCode block. | |
* @returns {object} the query object. | |
*/ | |
function getQuery(value) { | |
const hasHash = value.includes("#"); | |
const hasParams = value.includes("?"); | |
const split = value.split(/[?#]/); | |
const params = hasParams ? querystring.parse(split[hasHash ? 2 : 1]) : null; | |
const file = hasHash ? split[1] : params ? params.file : null; | |
// // if there is no file, then return an empty object | |
if (file == null) return { highlights: [], lines: [] }; | |
const query = { file, ...params }; | |
// validate the query | |
if (!isValid(query)) { | |
throw new Error("Malformed query. Check your 'gist:' imports"); | |
} | |
// explode the highlights ranges, if any | |
let highlights = []; | |
if (typeof query.highlights === "string") { | |
highlights = rangeParser.parse(query.highlights); | |
} else if (Array.isArray(query.highlights)) { | |
highlights = query.highlights; | |
} | |
query.highlights = highlights; | |
// get the range of lines to display | |
let lines = []; | |
if (typeof query.lines === "string") { | |
lines = rangeParser.parse(query.lines); | |
} else if (Array.isArray(query.lines)) { | |
lines = query.lines; | |
} | |
query.lines = lines; | |
return query; | |
} | |
/** | |
* Builds the gist url. | |
* @param {string} value the value of the inlineCode block. | |
* @param {PluginOptions} options the options of the plugin. | |
* @param {string} file the file to be loaded. | |
* @returns {string} the gist url. | |
*/ | |
function buildUrl(value, options, file) { | |
const [gist] = value.split(/[?#]/); | |
const [inlineUsername, id] = | |
gist.indexOf("/") > 0 ? gist.split("/") : [null, gist]; | |
// username can come from inline code or options | |
const username = inlineUsername || options.username; | |
// checks for a valid username | |
if (username == null || username.trim().length === 0) { | |
throw new Error("Missing username information"); | |
} | |
// checks for a valid id | |
if (id == null || id.trim().length === 0) { | |
throw new Error("Missing gist id information"); | |
} | |
// builds the url and completes it with the file if any | |
let url = `${baseUrl}/${username}/${id}.json`; | |
if (file != null) { | |
url += `?file=${file}`; | |
} | |
return url; | |
} | |
/** | |
* Handles the markdown AST. | |
* @param {{ markdownAST }} markdownAST the markdown abstract syntax tree. | |
* @param {PluginOptions} options the options of the plugin. | |
* @returns {*} the markdown ast. | |
*/ | |
export default async ({ markdownAST }, options = {}) => { | |
// this returns a promise that will fulfill immediately for everything | |
// that is not an inlineCode that starts with `gist:` | |
return await visit(markdownAST, "inlineCode", async node => { | |
// validate pre-requisites. | |
if (!node.value.startsWith("gist:")) return; | |
// get the query string and build the url | |
const query = getQuery(node.value.substring(5)); | |
const url = buildUrl(node.value.substring(5), options, query.file); | |
// call the gist and update the node type and value | |
const body = await request(url); | |
const content = JSON.parse(body); | |
let html = content.div; | |
const hasHighlights = query.highlights.length > 0; | |
const hasLines = query.lines.length > 0; | |
if (hasHighlights || hasLines) { | |
const $ = cheerio.load(html); | |
const file = query.file | |
? query.file.replace(/[^a-zA-Z0-9_]+/g, "-").toLowerCase() | |
: ""; | |
// highlight the specify lines, if any | |
if (hasHighlights) { | |
query.highlights.forEach(line => { | |
$(`#file-${file}-LC${line}`).addClass("highlighted"); | |
}); | |
} | |
// remove the specific lines, if any | |
if (hasLines) { | |
const codeLines = rangeParser.parse(`1-${$("table tr").length}`); | |
codeLines.forEach(line => { | |
if (query.lines.includes(line)) { | |
return; | |
} | |
$(`#file-${file}-LC${line}`) | |
.parent() | |
.remove(); | |
}); | |
} | |
html = $.html(); | |
} | |
node = Object.assign(node, { | |
type: "html", | |
value: html.trim() | |
}); | |
return markdownAST; | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment