mirror of
https://github.com/remoteintech/remote-jobs
synced 2025-01-22 17:15:28 +00:00
Fix formatting changes previously made
This commit is contained in:
parent
745db9c291
commit
cce5c06fae
1 changed files with 604 additions and 598 deletions
608
lib/index.js
608
lib/index.js
|
@ -1,164 +1,169 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require( 'fs' );
|
||||||
const path = require("path");
|
const path = require( 'path' );
|
||||||
const util = require("util");
|
const util = require( 'util' );
|
||||||
|
|
||||||
|
const cheerio = require( 'cheerio' );
|
||||||
|
const lunr = require( 'lunr' );
|
||||||
|
const marked = require( 'marked' );
|
||||||
|
|
||||||
const cheerio = require("cheerio");
|
|
||||||
const lunr = require("lunr");
|
|
||||||
const marked = require("marked");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants
|
* Constants
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const headingsRequired = ["Company blurb"];
|
const headingsRequired = [
|
||||||
const headingsOptional = [
|
'Company blurb',
|
||||||
"Company size",
|
|
||||||
"Remote status",
|
|
||||||
"Region",
|
|
||||||
"Company technologies",
|
|
||||||
"Office locations",
|
|
||||||
"How to apply",
|
|
||||||
];
|
];
|
||||||
const headingsAll = headingsRequired.concat(headingsOptional);
|
const headingsOptional = [
|
||||||
|
'Company size',
|
||||||
|
'Remote status',
|
||||||
|
'Region',
|
||||||
|
'Company technologies',
|
||||||
|
'Office locations',
|
||||||
|
'How to apply',
|
||||||
|
];
|
||||||
|
const headingsAll = headingsRequired.concat( headingsOptional );
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions
|
* Utility functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function companyNameToProfileFilename(companyName) {
|
function companyNameToProfileFilename( companyName ) {
|
||||||
return companyName
|
return companyName.toLowerCase()
|
||||||
.toLowerCase()
|
.replace( /&/g, ' and ' )
|
||||||
.replace(/&/g, " and ")
|
.replace( /'/g, '' )
|
||||||
.replace(/'/g, "")
|
.replace( /[^a-z0-9]+/gi, '-' )
|
||||||
.replace(/[^a-z0-9]+/gi, "-")
|
.replace( /^-|-$/g, '' );
|
||||||
.replace(/^-|-$/g, "");
|
|
||||||
}
|
}
|
||||||
exports.companyNameToProfileFilename = companyNameToProfileFilename;
|
exports.companyNameToProfileFilename = companyNameToProfileFilename;
|
||||||
|
|
||||||
// adapted from https://gist.github.com/RandomEtc/2657669
|
// adapted from https://gist.github.com/RandomEtc/2657669
|
||||||
function jsonStringifyUnicodeEscaped(obj) {
|
function jsonStringifyUnicodeEscaped( obj ) {
|
||||||
return JSON.stringify(obj).replace(/[\u007f-\uffff]/g, (c) => {
|
return JSON.stringify( obj ).replace( /[\u007f-\uffff]/g, c => {
|
||||||
return "\\u" + ("0000" + c.charCodeAt(0).toString(16)).slice(-4);
|
return '\\u' + ( '0000' + c.charCodeAt( 0 ).toString( 16 ) ).slice( -4 );
|
||||||
});
|
} );
|
||||||
}
|
}
|
||||||
exports.jsonStringifyUnicodeEscaped = jsonStringifyUnicodeEscaped;
|
exports.jsonStringifyUnicodeEscaped = jsonStringifyUnicodeEscaped;
|
||||||
|
|
||||||
function toIdentifierCase(text) {
|
function toIdentifierCase( text ) {
|
||||||
return text
|
return text
|
||||||
.replace(/'/g, "")
|
.replace( /'/g, '' )
|
||||||
.replace(/[^a-z0-9]+/gi, " ")
|
.replace( /[^a-z0-9]+/gi, ' ' )
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split( /\s+/ )
|
||||||
.map((word, i) => {
|
.map( ( word, i ) => {
|
||||||
if (i === 0) {
|
if ( i === 0 ) {
|
||||||
return word.toLowerCase();
|
return word.toLowerCase();
|
||||||
}
|
}
|
||||||
return word.substr(0, 1).toUpperCase() + word.substr(1).toLowerCase();
|
return (
|
||||||
})
|
word.substr( 0, 1 ).toUpperCase()
|
||||||
.join("");
|
+ word.substr( 1 ).toLowerCase()
|
||||||
|
);
|
||||||
|
} )
|
||||||
|
.join( '' );
|
||||||
}
|
}
|
||||||
exports.toIdentifierCase = toIdentifierCase;
|
exports.toIdentifierCase = toIdentifierCase;
|
||||||
|
|
||||||
function stripExtraChars(text) {
|
function stripExtraChars( text ) {
|
||||||
return text.replace(/\ufe0f/g, "");
|
return text.replace( /\ufe0f/g, '' );
|
||||||
}
|
}
|
||||||
exports.stripExtraChars = stripExtraChars;
|
exports.stripExtraChars = stripExtraChars;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Other exports
|
* Other exports
|
||||||
*/
|
*/
|
||||||
function getHeadingPropertyNames() {
|
function getHeadingPropertyNames() {
|
||||||
return headingsAll.reduce((acc, val) => {
|
return headingsAll.reduce( ( acc, val ) => {
|
||||||
acc[toIdentifierCase(val)] = val;
|
acc[ toIdentifierCase( val ) ] = val;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {} );
|
||||||
}
|
}
|
||||||
exports.headingPropertyNames = getHeadingPropertyNames();
|
exports.headingPropertyNames = getHeadingPropertyNames();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main exported function
|
* The main exported function
|
||||||
*
|
*
|
||||||
* Start with a directory including a README.md and company-profiles/*.md
|
* Start with a directory including a README.md and company-profiles/*.md
|
||||||
* files, and validate and parse the content of the Markdown files.
|
* files, and validate and parse the content of the Markdown files.
|
||||||
*/
|
*/
|
||||||
exports.parseFromDirectory = (contentPath) => {
|
exports.parseFromDirectory = contentPath => {
|
||||||
const companyNamesSeen = {};
|
const companyNamesSeen = {};
|
||||||
let errors = [];
|
let errors = [];
|
||||||
|
|
||||||
function error(filename, msg, ...params) {
|
function error( filename, msg, ...params ) {
|
||||||
errors.push({
|
errors.push( {
|
||||||
filename,
|
filename,
|
||||||
message: util.format(msg, ...params),
|
message: util.format( msg, ...params ),
|
||||||
});
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build list of Markdown files containing company profiles.
|
// Build list of Markdown files containing company profiles.
|
||||||
const profilesPath = path.join(contentPath, "company-profiles");
|
const profilesPath = path.join( contentPath, 'company-profiles' );
|
||||||
const profileFilenames = fs.readdirSync(profilesPath);
|
const profileFilenames = fs.readdirSync( profilesPath );
|
||||||
|
|
||||||
// Scan the company table in the readme.
|
// Scan the company table in the readme.
|
||||||
|
|
||||||
const readmeCompanies = [];
|
const readmeCompanies = [];
|
||||||
|
|
||||||
const readmeMarkdown = stripExtraChars(
|
const readmeMarkdown = stripExtraChars( fs.readFileSync(
|
||||||
fs.readFileSync(path.join(contentPath, "README.md"), "utf8")
|
path.join( contentPath, 'README.md' ),
|
||||||
);
|
'utf8'
|
||||||
|
) );
|
||||||
|
|
||||||
let inTable = false;
|
let inTable = false;
|
||||||
readmeMarkdown.split("\n").forEach((line) => {
|
readmeMarkdown.split( '\n' ).forEach( line => {
|
||||||
if (/^\s*-+\s*\|\s*-+\s*\|\s*-+\s*$/.test(line)) {
|
if ( /^\s*-+\s*\|\s*-+\s*\|\s*-+\s*$/.test( line ) ) {
|
||||||
inTable = true;
|
inTable = true;
|
||||||
} else if (/^\s*$/.test(line)) {
|
} else if ( /^\s*$/.test( line ) ) {
|
||||||
inTable = false;
|
inTable = false;
|
||||||
} else if (inTable) {
|
} else if ( inTable ) {
|
||||||
const fields = line.split("|");
|
const fields = line.split( '|' );
|
||||||
if (fields.length !== 3) {
|
if ( fields.length !== 3 ) {
|
||||||
readmeError(
|
readmeError(
|
||||||
"Expected 3 table cells but found %d: %s",
|
'Expected 3 table cells but found %d: %s',
|
||||||
fields.length,
|
fields.length,
|
||||||
line
|
line
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
const $ = cheerio.load(marked.parse(readmeMarkdown));
|
const $ = cheerio.load( marked.parse( readmeMarkdown ) );
|
||||||
|
|
||||||
function readmeError(msg, ...params) {
|
function readmeError( msg, ...params ) {
|
||||||
error("README.md", msg, ...params);
|
error( 'README.md', msg, ...params );
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainUrl = "remoteintech.company";
|
const mainUrl = 'remoteintech.company'
|
||||||
|
|
||||||
function addTargetBlankAndExternalLinkIcons(el) {
|
function addTargetBlankAndExternalLinkIcons (el) {
|
||||||
if (el.type === "tag") {
|
if (el.type === 'tag') {
|
||||||
const anchorTagElements = el.children.filter(
|
const anchorTagElements = el.children.filter(element => element.name === 'a')
|
||||||
(element) => element.name === "a"
|
|
||||||
);
|
|
||||||
if (anchorTagElements.length > 0) {
|
if (anchorTagElements.length > 0) {
|
||||||
anchorTagElements.forEach((element) => {
|
anchorTagElements.forEach(element => {
|
||||||
const url = element.attribs.href;
|
const url = element.attribs.href
|
||||||
const urlInfo = getUrlInfo(url);
|
const urlInfo = getUrlInfo(url)
|
||||||
|
|
||||||
if (urlInfo.is_email || urlInfo.is_internal) {
|
if (urlInfo.is_email || urlInfo.is_internal) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
element.attribs.target = "_blank";
|
element.attribs.target = '_blank'
|
||||||
|
|
||||||
$element = $(element);
|
$element = $( element )
|
||||||
$element.append(
|
$element.append('<span style="vertical-align: text-top;"> <img src="/assets/external-link.svg" /> </span>')
|
||||||
'<span style="vertical-align: text-top;"> <img src="/assets/external-link.svg" /> </span>'
|
})
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.children && el.children.length) {
|
if (el.children && el.children.length) {
|
||||||
el.children.forEach((element) => {
|
el.children.forEach(element => {
|
||||||
addTargetBlankAndExternalLinkIcons(element);
|
addTargetBlankAndExternalLinkIcons(element)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,110 +172,109 @@ exports.parseFromDirectory = (contentPath) => {
|
||||||
* Getting info about the url. It includes checking isEmail of isInternal
|
* Getting info about the url. It includes checking isEmail of isInternal
|
||||||
* @param {*} url
|
* @param {*} url
|
||||||
*/
|
*/
|
||||||
function getUrlInfo(url) {
|
function getUrlInfo (url) {
|
||||||
const data = {};
|
const data = {}
|
||||||
|
|
||||||
if (url.match(/^mailto:/)) {
|
if (url.match(/^mailto:/)) { // checking url email or not
|
||||||
// checking url email or not
|
data.is_email = true
|
||||||
data.is_email = true;
|
return data
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainDomainFromGivenUrl = extractMainDomainFromUrl(url);
|
const mainDomainFromGivenUrl = extractMainDomainFromUrl(url)
|
||||||
|
|
||||||
// checking url is email or not
|
// checking url is email or not
|
||||||
if (mainDomainFromGivenUrl !== mainUrl) {
|
if (mainDomainFromGivenUrl !== mainUrl) {
|
||||||
data.is_internal = false;
|
data.is_internal = false
|
||||||
return data;
|
return data
|
||||||
} else {
|
} else {
|
||||||
data.is_internal = true;
|
data.is_internal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracting main domain from the url
|
* Extracting main domain from the url
|
||||||
* @param {*} url
|
* @param {*} url
|
||||||
*/
|
*/
|
||||||
function extractMainDomainFromUrl(url) {
|
function extractMainDomainFromUrl (url) {
|
||||||
const domainRe = /(https?:\/\/){0,1}((?:[\w\d-]+\.)+[\w\d]{2,})/i; // taken example from https://stackoverflow.com/questions/6238351/fastest-way-to-detect-external-urls
|
const domainRe = /(https?:\/\/){0,1}((?:[\w\d-]+\.)+[\w\d]{2,})/i; // taken example from https://stackoverflow.com/questions/6238351/fastest-way-to-detect-external-urls
|
||||||
|
|
||||||
const data = domainRe.exec(url);
|
const data = domainRe.exec(url)
|
||||||
|
|
||||||
const splittedDomain = data[2].split(".");
|
const splittedDomain = data[2].split('.')
|
||||||
|
|
||||||
if (splittedDomain.length === 2) {
|
if (splittedDomain.length === 2) { // check extra subdomain is present or not
|
||||||
// check extra subdomain is present or not
|
return data[2]
|
||||||
return data[2];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return splittedDomain[splittedDomain.length - 2] + '.' + splittedDomain[splittedDomain.length - 1] // return only main domain address
|
||||||
splittedDomain[splittedDomain.length - 2] +
|
|
||||||
"." +
|
|
||||||
splittedDomain[splittedDomain.length - 1]
|
|
||||||
); // return only main domain address
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastCompanyName = null;
|
let lastCompanyName = null;
|
||||||
|
|
||||||
$("tr").each((i, tr) => {
|
$( 'tr' ).each( ( i, tr ) => {
|
||||||
const $tr = $(tr);
|
const $tr = $( tr );
|
||||||
if (i === 0) {
|
if ( i === 0 ) {
|
||||||
// Assign an ID to the table.
|
// Assign an ID to the table.
|
||||||
$tr.closest("table").attr("id", "companies-table");
|
$tr.closest( 'table' ).attr( 'id', 'companies-table' );
|
||||||
// Skip the table header row.
|
// Skip the table header row.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const $td = $tr.children("td");
|
const $td = $tr.children( 'td' );
|
||||||
|
|
||||||
const websiteUrl = $td.eq(1).text();
|
const websiteUrl = $td.eq( 1 ).text();
|
||||||
const websiteText = websiteUrl
|
const websiteText = websiteUrl
|
||||||
.replace(/^https?:\/\//, "")
|
.replace( /^https?:\/\//, '' )
|
||||||
.replace(/^www\./, "")
|
.replace( /^www\./, '' )
|
||||||
.replace(/\/$/, "");
|
.replace( /\/$/, '' );
|
||||||
|
|
||||||
const readmeEntry = {
|
const readmeEntry = {
|
||||||
// Strip out warning emoji indicating that this profile is incomplete
|
// Strip out warning emoji indicating that this profile is incomplete
|
||||||
name: $td
|
name: $td.eq( 0 ).text().replace( /\u26a0/, '' ).trim(),
|
||||||
.eq(0)
|
|
||||||
.text()
|
|
||||||
.replace(/\u26a0/, "")
|
|
||||||
.trim(),
|
|
||||||
// Detect warning emoji next to company name
|
// Detect warning emoji next to company name
|
||||||
isIncomplete: /\u26a0/.test($td.eq(0).text()),
|
isIncomplete: /\u26a0/.test( $td.eq( 0 ).text() ),
|
||||||
websiteUrl,
|
websiteUrl,
|
||||||
websiteText,
|
websiteText,
|
||||||
shortRegion: $td.eq(2).text().trim(),
|
shortRegion: $td.eq( 2 ).text().trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!websiteText) {
|
if ( ! websiteText ) {
|
||||||
readmeError("Missing website for company: %s", readmeEntry.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readmeEntry.name) {
|
|
||||||
if (companyNamesSeen[readmeEntry.name.toLowerCase()]) {
|
|
||||||
readmeError("Duplicate company: %s", readmeEntry.name);
|
|
||||||
}
|
|
||||||
companyNamesSeen[readmeEntry.name.toLowerCase()] = true;
|
|
||||||
} else {
|
|
||||||
readmeError("Missing company name: %s", $tr.html().replace(/\n/g, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
$td.eq(1).children().length !== 1 ||
|
|
||||||
!$td.eq(1).children().eq(0).is("a")
|
|
||||||
) {
|
|
||||||
readmeError(
|
readmeError(
|
||||||
"Invalid content in Website column: %s",
|
'Missing website for company: %s',
|
||||||
$tr.html().replace(/\n/g, "")
|
readmeEntry.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($td.eq(2).children().length > 0) {
|
if ( readmeEntry.name ) {
|
||||||
|
if ( companyNamesSeen[ readmeEntry.name.toLowerCase() ] ) {
|
||||||
readmeError(
|
readmeError(
|
||||||
"Extra content in Region column: %s",
|
'Duplicate company: %s',
|
||||||
$tr.html().replace(/\n/g, "")
|
readmeEntry.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
companyNamesSeen[ readmeEntry.name.toLowerCase() ] = true;
|
||||||
|
} else {
|
||||||
|
readmeError(
|
||||||
|
'Missing company name: %s',
|
||||||
|
$tr.html().replace( /\n/g, '' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$td.eq( 1 ).children().length !== 1 ||
|
||||||
|
! $td.eq( 1 ).children().eq( 0 ).is( 'a' )
|
||||||
|
) {
|
||||||
|
readmeError(
|
||||||
|
'Invalid content in Website column: %s',
|
||||||
|
$tr.html().replace( /\n/g, '' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $td.eq( 2 ).children().length > 0 ) {
|
||||||
|
readmeError(
|
||||||
|
'Extra content in Region column: %s',
|
||||||
|
$tr.html().replace( /\n/g, '' )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,119 +290,123 @@ exports.parseFromDirectory = (contentPath) => {
|
||||||
}
|
}
|
||||||
lastCompanyName = readmeEntry.name;
|
lastCompanyName = readmeEntry.name;
|
||||||
|
|
||||||
const $profileLink = $td.eq(0).find("a");
|
const $profileLink = $td.eq( 0 ).find( 'a' );
|
||||||
|
|
||||||
if ($profileLink.length === 1) {
|
if ( $profileLink.length === 1 ) {
|
||||||
const match = $profileLink
|
const match = $profileLink.attr( 'href' ).match( /^\/company-profiles\/(.*\.md)$/ );
|
||||||
.attr("href")
|
|
||||||
.match(/^\/company-profiles\/(.*\.md)$/);
|
|
||||||
|
|
||||||
if (match) {
|
if ( match ) {
|
||||||
readmeEntry.linkedFilename = match[1];
|
readmeEntry.linkedFilename = match[ 1 ];
|
||||||
if (profileFilenames.indexOf(readmeEntry.linkedFilename) === -1) {
|
if ( profileFilenames.indexOf( readmeEntry.linkedFilename ) === -1 ) {
|
||||||
readmeError(
|
readmeError(
|
||||||
'Missing company profile for "%s", or broken link: "%s"',
|
'Missing company profile for "%s", or broken link: "%s"',
|
||||||
readmeEntry.name,
|
readmeEntry.name,
|
||||||
$profileLink.attr("href")
|
$profileLink.attr( 'href' )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameCheck = $profileLink.text().trim();
|
const nameCheck = $profileLink.text().trim();
|
||||||
if (nameCheck !== readmeEntry.name) {
|
if ( nameCheck !== readmeEntry.name ) {
|
||||||
readmeError(
|
readmeError(
|
||||||
"Extra text in company name: %s, %s",
|
'Extra text in company name: %s, %s',
|
||||||
jsonStringifyUnicodeEscaped(nameCheck),
|
jsonStringifyUnicodeEscaped( nameCheck ),
|
||||||
jsonStringifyUnicodeEscaped(readmeEntry.name)
|
jsonStringifyUnicodeEscaped( readmeEntry.name )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
readmeError(
|
readmeError(
|
||||||
'Invalid link to company profile for "%s": "%s"',
|
'Invalid link to company profile for "%s": "%s"',
|
||||||
readmeEntry.name,
|
readmeEntry.name,
|
||||||
$profileLink.attr("href")
|
$profileLink.attr( 'href' )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
readmeError(
|
readmeError(
|
||||||
'Company "%s" has no linked Markdown profile ("/company-profiles/%s.md")',
|
'Company "%s" has no linked Markdown profile ("/company-profiles/%s.md")',
|
||||||
readmeEntry.name,
|
readmeEntry.name,
|
||||||
companyNameToProfileFilename(readmeEntry.name)
|
companyNameToProfileFilename( readmeEntry.name )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set identifying attributes on table elements
|
// Set identifying attributes on table elements
|
||||||
$tr.attr("class", "company-row").attr("id", "company-row-" + (i - 1));
|
$tr
|
||||||
$td.eq(0).attr("class", "company-name");
|
.attr( 'class', 'company-row' )
|
||||||
$td.eq(1).attr("class", "company-website");
|
.attr( 'id', 'company-row-' + ( i - 1 ) );
|
||||||
$td.eq(2).attr("class", "company-region");
|
$td.eq( 0 ).attr( 'class', 'company-name' );
|
||||||
|
$td.eq( 1 ).attr( 'class', 'company-website' );
|
||||||
|
$td.eq( 2 ).attr( 'class', 'company-region' );
|
||||||
|
|
||||||
// Rewrite company profile link to the correct URL for the static site
|
// Rewrite company profile link to the correct URL for the static site
|
||||||
if ($profileLink.length) {
|
if ( $profileLink.length ) {
|
||||||
$profileLink.attr(
|
$profileLink.attr(
|
||||||
"href",
|
'href',
|
||||||
$profileLink
|
$profileLink.attr( 'href' )
|
||||||
.attr("href")
|
.replace( /^\/company-profiles\//, '/' )
|
||||||
.replace(/^\/company-profiles\//, "/")
|
.replace( /\.md$/, '/' )
|
||||||
.replace(/\.md$/, "/")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite external website link (target="_blank" etc, shorter text)
|
// Rewrite external website link (target="_blank" etc, shorter text)
|
||||||
const $websiteLink = $td.eq(1).children().eq(0);
|
const $websiteLink = $td.eq( 1 ).children().eq( 0 );
|
||||||
$websiteLink
|
$websiteLink
|
||||||
.attr("target", "_blank")
|
.attr( 'target', '_blank' )
|
||||||
.attr("rel", "noopener noreferrer")
|
.attr( 'rel', 'noopener noreferrer' )
|
||||||
.text(websiteText);
|
.text( websiteText );
|
||||||
|
|
||||||
readmeCompanies.push(readmeEntry);
|
readmeCompanies.push( readmeEntry );
|
||||||
});
|
} );
|
||||||
|
|
||||||
const readmeContent = $("body").html();
|
const readmeContent = $( 'body' ).html();
|
||||||
|
|
||||||
// Scan the individual Markdown files containing the company profiles.
|
// Scan the individual Markdown files containing the company profiles.
|
||||||
|
|
||||||
const allProfileHeadings = {};
|
const allProfileHeadings = {};
|
||||||
|
|
||||||
profileFilenames.forEach((filename) => {
|
profileFilenames.forEach( filename => {
|
||||||
function profileError(msg, ...params) {
|
function profileError( msg, ...params ) {
|
||||||
error(filename, msg, ...params);
|
error( filename, msg, ...params );
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileMarkdown = stripExtraChars(
|
const profileMarkdown = stripExtraChars( fs.readFileSync(
|
||||||
fs.readFileSync(path.join(profilesPath, filename), "utf8")
|
path.join( profilesPath, filename ),
|
||||||
);
|
'utf8'
|
||||||
const $ = cheerio.load(marked.parse(profileMarkdown));
|
) );
|
||||||
|
const $ = cheerio.load( marked.parse( profileMarkdown ) );
|
||||||
|
|
||||||
let hasTitleError = false;
|
let hasTitleError = false;
|
||||||
|
|
||||||
if ($("h1").length !== 1) {
|
if ( $( 'h1' ).length !== 1 ) {
|
||||||
profileError(
|
profileError(
|
||||||
"Expected 1 first-level heading but found %d",
|
'Expected 1 first-level heading but found %d',
|
||||||
$("h1").length
|
$( 'h1' ).length
|
||||||
);
|
);
|
||||||
hasTitleError = true;
|
hasTitleError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$("h1").parent().is("body")) {
|
if ( ! $( 'h1' ).parent().is( 'body' ) ) {
|
||||||
profileError("The main title is wrapped inside of another element.");
|
profileError(
|
||||||
|
'The main title is wrapped inside of another element.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyName = $("h1").text();
|
const companyName = $( 'h1' ).text();
|
||||||
|
|
||||||
if (!/[a-z]/i.test(companyName)) {
|
if ( ! /[a-z]/i.test( companyName ) ) {
|
||||||
profileError('Company name looks wrong: "%s"', companyName);
|
profileError(
|
||||||
|
'Company name looks wrong: "%s"',
|
||||||
|
companyName
|
||||||
|
);
|
||||||
hasTitleError = true;
|
hasTitleError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filenameBase = filename.replace(/\.md$/, "");
|
const filenameBase = filename.replace( /\.md$/, '' );
|
||||||
const filenameExpected = companyNameToProfileFilename(companyName);
|
const filenameExpected = companyNameToProfileFilename( companyName );
|
||||||
if (
|
if (
|
||||||
!hasTitleError &&
|
! hasTitleError &&
|
||||||
filenameBase !== filenameExpected &&
|
filenameBase !== filenameExpected &&
|
||||||
// Some profile files just have shorter names than the company name,
|
// Some profile files just have shorter names than the company name,
|
||||||
// which is fine.
|
// which is fine.
|
||||||
filenameExpected.substring(0, filenameBase.length + 1) !==
|
filenameExpected.substring( 0, filenameBase.length + 1 ) !== filenameBase + '-'
|
||||||
filenameBase + "-"
|
|
||||||
) {
|
) {
|
||||||
profileError(
|
profileError(
|
||||||
'Company title "%s" doesn\'t match filename (expected ~ "%s.md")',
|
'Company title "%s" doesn\'t match filename (expected ~ "%s.md")',
|
||||||
|
@ -408,177 +416,177 @@ exports.parseFromDirectory = (contentPath) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const readmeEntry = readmeCompanies.find(
|
const readmeEntry = readmeCompanies.find(
|
||||||
(readmeEntry) => readmeEntry.linkedFilename === filename
|
readmeEntry => readmeEntry.linkedFilename === filename
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filename !== "example.md" && !readmeEntry) {
|
if ( filename !== 'example.md' && ! readmeEntry ) {
|
||||||
profileError("No link to company profile from readme");
|
profileError( 'No link to company profile from readme' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build and validate list of headings contained in this Markdown profile.
|
// Build and validate list of headings contained in this Markdown profile.
|
||||||
|
|
||||||
const profileHeadings = [];
|
const profileHeadings = [];
|
||||||
|
|
||||||
$("h2").each((i, el) => {
|
$( 'h2' ).each( ( i, el ) => {
|
||||||
const headingName = $(el).html();
|
const headingName = $( el ).html();
|
||||||
|
|
||||||
if (!$(el).parent().is("body")) {
|
if ( ! $( el ).parent().is( 'body' ) ) {
|
||||||
profileError(
|
profileError(
|
||||||
'The section heading for "%s" is wrapped inside of another element.',
|
'The section heading for "%s" is wrapped inside of another element.',
|
||||||
headingName
|
headingName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileHeadings.indexOf(headingName) >= 0) {
|
if ( profileHeadings.indexOf( headingName ) >= 0 ) {
|
||||||
profileError('Duplicate section: "%s".', headingName);
|
profileError(
|
||||||
|
'Duplicate section: "%s".',
|
||||||
|
headingName
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Track headings for this profile
|
// Track headings for this profile
|
||||||
profileHeadings.push(headingName);
|
profileHeadings.push( headingName );
|
||||||
|
|
||||||
// Track heading counts across all profiles
|
// Track heading counts across all profiles
|
||||||
if (!allProfileHeadings[headingName]) {
|
if ( ! allProfileHeadings[ headingName ] ) {
|
||||||
allProfileHeadings[headingName] = [];
|
allProfileHeadings[ headingName ] = [];
|
||||||
}
|
}
|
||||||
allProfileHeadings[headingName].push(filename);
|
allProfileHeadings[ headingName ].push( filename );
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headingsAll.indexOf(headingName) === -1) {
|
if ( headingsAll.indexOf( headingName ) === -1 ) {
|
||||||
profileError(
|
profileError(
|
||||||
'Invalid section: "%s". Expected one of: %s',
|
'Invalid section: "%s". Expected one of: %s',
|
||||||
headingName,
|
headingName,
|
||||||
JSON.stringify(headingsAll)
|
JSON.stringify( headingsAll )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
headingsRequired.forEach((headingName) => {
|
headingsRequired.forEach( headingName => {
|
||||||
if (profileHeadings.indexOf(headingName) === -1) {
|
if ( profileHeadings.indexOf( headingName ) === -1 ) {
|
||||||
profileError('Required section "%s" not found.', headingName);
|
profileError(
|
||||||
|
'Required section "%s" not found.',
|
||||||
|
headingName
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
// Build and validate the content of each section in this profile.
|
// Build and validate the content of each section in this profile.
|
||||||
|
|
||||||
const profileContent = {};
|
const profileContent = {};
|
||||||
if (readmeEntry) {
|
if ( readmeEntry ) {
|
||||||
readmeEntry.profileContent = profileContent;
|
readmeEntry.profileContent = profileContent;
|
||||||
}
|
}
|
||||||
let currentHeading = null;
|
let currentHeading = null;
|
||||||
|
|
||||||
$("body")
|
$( 'body' ).children().each( ( i, el ) => {
|
||||||
.children()
|
const $el = $( el );
|
||||||
.each((i, el) => {
|
|
||||||
const $el = $(el);
|
|
||||||
|
|
||||||
if ($el.is("h1")) {
|
if ( $el.is( 'h1' ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($el.is("h2")) {
|
if ( $el.is( 'h2' ) ) {
|
||||||
currentHeading = $el.html();
|
currentHeading = $el.html();
|
||||||
profileContent[currentHeading] = "";
|
profileContent[ currentHeading ] = '';
|
||||||
} else if (currentHeading) {
|
} else if ( currentHeading ) {
|
||||||
// Note: This assumes that the only possible children of the
|
// Note: This assumes that the only possible children of the
|
||||||
// 'body' are block-level elements. I think this is correct,
|
// 'body' are block-level elements. I think this is correct,
|
||||||
// because from what I've seen, any inline content is wrapped
|
// because from what I've seen, any inline content is wrapped
|
||||||
// in a <p>.
|
// in a <p>.
|
||||||
addTargetBlankAndExternalLinkIcons(el);
|
addTargetBlankAndExternalLinkIcons(el)
|
||||||
profileContent[currentHeading] = (
|
profileContent[ currentHeading ] = (
|
||||||
profileContent[currentHeading] +
|
profileContent[ currentHeading ]
|
||||||
"\n\n" +
|
+ '\n\n' + $.html( el )
|
||||||
$.html(el)
|
|
||||||
).trim();
|
).trim();
|
||||||
} else {
|
} else {
|
||||||
profileError(
|
profileError(
|
||||||
"Content is not part of any section: %s",
|
'Content is not part of any section: %s',
|
||||||
$.html(el).replace(/\n/g, "")
|
$.html( el ).replace( /\n/g, '' )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
Object.keys(profileContent).forEach((heading) => {
|
Object.keys( profileContent ).forEach( heading => {
|
||||||
const sectionText = profileContent[heading]
|
const sectionText = profileContent[ heading ]
|
||||||
.replace(/<[^>]+>/g, "")
|
.replace( /<[^>]+>/g, '' )
|
||||||
.trim();
|
.trim();
|
||||||
if (!sectionText) {
|
if ( ! sectionText ) {
|
||||||
profileError(
|
profileError(
|
||||||
'Empty section: "%s". Fill it in or leave it out instead.',
|
'Empty section: "%s". Fill it in or leave it out instead.',
|
||||||
heading
|
heading
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
// Rewrite profile content to use more code-friendly heading names.
|
// Rewrite profile content to use more code-friendly heading names.
|
||||||
Object.keys(profileContent).forEach((headingName) => {
|
Object.keys( profileContent ).forEach( headingName => {
|
||||||
const headingIdentifier = toIdentifierCase(headingName);
|
const headingIdentifier = toIdentifierCase( headingName );
|
||||||
profileContent[headingIdentifier] = profileContent[headingName];
|
profileContent[ headingIdentifier ] = profileContent[ headingName ];
|
||||||
delete profileContent[headingName];
|
delete profileContent[ headingName ];
|
||||||
});
|
} );
|
||||||
|
|
||||||
if (readmeEntry && profileContent.companyBlurb) {
|
if ( readmeEntry && profileContent.companyBlurb ) {
|
||||||
// Check for company profiles that were filled in, but the "incomplete"
|
// Check for company profiles that were filled in, but the "incomplete"
|
||||||
// mark was left in the readme, or vice versa.
|
// mark was left in the readme, or vice versa.
|
||||||
const isIncomplete = {
|
const isIncomplete = {
|
||||||
readme: readmeEntry.isIncomplete,
|
readme: readmeEntry.isIncomplete,
|
||||||
sections:
|
sections: (
|
||||||
profileHeadings.length === 1 &&
|
profileHeadings.length === 1 &&
|
||||||
profileHeadings[0] === "Company blurb",
|
profileHeadings[ 0 ] === 'Company blurb'
|
||||||
content: /⚠/.test(profileContent.companyBlurb),
|
),
|
||||||
|
content: /⚠/.test( profileContent.companyBlurb ),
|
||||||
};
|
};
|
||||||
const incompleteCount = Object.values(isIncomplete).reduce(
|
const incompleteCount = Object.values( isIncomplete )
|
||||||
(sum, v) => sum + (v ? 1 : 0),
|
.reduce( ( sum, v ) => sum + ( v ? 1 : 0 ), 0 );
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// incompleteCount === 0: Profile is incomplete; all 3 indicators are consistent
|
// incompleteCount === 0: Profile is incomplete; all 3 indicators are consistent
|
||||||
// incompleteCount === 3: Profile is "complete"; all 3 indicators are consistent
|
// incompleteCount === 3: Profile is "complete"; all 3 indicators are consistent
|
||||||
if (incompleteCount === 1) {
|
if ( incompleteCount === 1 ) {
|
||||||
if (isIncomplete.readme) {
|
if ( isIncomplete.readme ) {
|
||||||
profileError(
|
profileError(
|
||||||
"Profile looks complete, but the main readme contains a warning emoji."
|
'Profile looks complete, but the main readme contains a warning emoji.'
|
||||||
);
|
);
|
||||||
} else if (isIncomplete.sections) {
|
} else if ( isIncomplete.sections ) {
|
||||||
profileError(
|
profileError(
|
||||||
'Profile is marked as complete, but it only contains a "Company blurb" heading.'
|
'Profile is marked as complete, but it only contains a "Company blurb" heading.'
|
||||||
);
|
)
|
||||||
} else {
|
} else { // isIncomplete.content
|
||||||
// isIncomplete.content
|
|
||||||
profileError(
|
profileError(
|
||||||
'Profile looks complete, but the "Company blurb" contains a warning emoji.'
|
'Profile looks complete, but the "Company blurb" contains a warning emoji.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (incompleteCount === 2) {
|
} else if ( incompleteCount === 2 ) {
|
||||||
if (!isIncomplete.readme) {
|
if ( ! isIncomplete.readme ) {
|
||||||
profileError(
|
profileError(
|
||||||
"Profile looks incomplete, but the main readme does not contain a warning emoji."
|
'Profile looks incomplete, but the main readme does not contain a warning emoji.'
|
||||||
);
|
);
|
||||||
} else if (!isIncomplete.sections) {
|
} else if ( ! isIncomplete.sections ) {
|
||||||
profileError(
|
profileError(
|
||||||
"Profile is marked as incomplete, but it contains multiple sections." +
|
'Profile is marked as incomplete, but it contains multiple sections.'
|
||||||
'\nPlease remove the warning emoji from the "Company blurb" section and the main readme.'
|
+ '\nPlease remove the warning emoji from the "Company blurb" section and the main readme.'
|
||||||
);
|
)
|
||||||
} else {
|
} else { // ! isIncomplete.content
|
||||||
// ! isIncomplete.content
|
|
||||||
profileError(
|
profileError(
|
||||||
'Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.'
|
'Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
const profileHeadingCounts = {};
|
const profileHeadingCounts = {};
|
||||||
Object.keys(allProfileHeadings).forEach((heading) => {
|
Object.keys( allProfileHeadings ).forEach( heading => {
|
||||||
profileHeadingCounts[heading] = allProfileHeadings[heading].length;
|
profileHeadingCounts[ heading ] = allProfileHeadings[ heading ].length;
|
||||||
});
|
} );
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if ( errors.length > 0 ) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
errors,
|
errors,
|
||||||
profileFilenames,
|
profileFilenames,
|
||||||
profileHeadingCounts,
|
profileHeadingCounts,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -593,63 +601,61 @@ exports.parseFromDirectory = (contentPath) => {
|
||||||
/**
|
/**
|
||||||
* Build search index data from the result of parseFromDirectory().
|
* Build search index data from the result of parseFromDirectory().
|
||||||
*/
|
*/
|
||||||
exports.buildSearchData = (data) => {
|
exports.buildSearchData = data => {
|
||||||
const textData = [];
|
const textData = [];
|
||||||
|
|
||||||
data.companies.forEach((company, i) => {
|
data.companies.forEach( ( company, i ) => {
|
||||||
const thisTextData = {
|
const thisTextData = {
|
||||||
id: String(i),
|
id: String( i ),
|
||||||
nameText: company.name,
|
nameText: company.name,
|
||||||
websiteText: company.websiteText,
|
websiteText: company.websiteText,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (company.shortRegion) {
|
if ( company.shortRegion ) {
|
||||||
thisTextData.shortRegion = company.shortRegion;
|
thisTextData.shortRegion = company.shortRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(exports.headingPropertyNames).forEach((h) => {
|
Object.keys( exports.headingPropertyNames ).forEach( h => {
|
||||||
if (company.profileContent[h]) {
|
if ( company.profileContent[ h ] ) {
|
||||||
const text = cheerio
|
const text = cheerio.load( company.profileContent[ h ] ).text()
|
||||||
.load(company.profileContent[h])
|
|
||||||
.text()
|
|
||||||
// Replace warning emoji with a searchable token
|
// Replace warning emoji with a searchable token
|
||||||
.replace(/\u26a0/, "(_incomplete)");
|
.replace( /\u26a0/, '(_incomplete)' );
|
||||||
thisTextData[h] = text;
|
thisTextData[ h ] = text;
|
||||||
}
|
}
|
||||||
});
|
} );
|
||||||
|
|
||||||
textData.push(thisTextData);
|
textData.push( thisTextData );
|
||||||
});
|
} );
|
||||||
|
|
||||||
const index = lunr(function () {
|
const index = lunr( function() {
|
||||||
this.field("nameText");
|
this.field( 'nameText' );
|
||||||
this.field("websiteText");
|
this.field( 'websiteText' );
|
||||||
this.field("shortRegion");
|
this.field( 'shortRegion' );
|
||||||
|
|
||||||
Object.keys(exports.headingPropertyNames).forEach((h) => {
|
Object.keys( exports.headingPropertyNames ).forEach( h => {
|
||||||
this.field(h);
|
this.field( h );
|
||||||
});
|
} );
|
||||||
|
|
||||||
// https://github.com/olivernn/lunr.js/issues/25#issuecomment-623267494
|
// https://github.com/olivernn/lunr.js/issues/25#issuecomment-623267494
|
||||||
this.metadataWhitelist = ["position"];
|
this.metadataWhitelist = ['position'];
|
||||||
|
|
||||||
// https://github.com/olivernn/lunr.js/issues/192#issuecomment-172915226
|
// https://github.com/olivernn/lunr.js/issues/192#issuecomment-172915226
|
||||||
// https://gist.github.com/olivernn/7cd496f8654a0246c53c
|
// https://gist.github.com/olivernn/7cd496f8654a0246c53c
|
||||||
function contractionTrimmer(token) {
|
function contractionTrimmer( token ) {
|
||||||
return token.update((str) => {
|
return token.update( str => {
|
||||||
return str.replace(/('m|'ve|n't|'d|'ll|'ve|'s|'re)$/, "");
|
return str.replace( /('m|'ve|n't|'d|'ll|'ve|'s|'re)$/, '' );
|
||||||
});
|
} );
|
||||||
}
|
}
|
||||||
lunr.Pipeline.registerFunction(contractionTrimmer, "contractionTrimmer");
|
lunr.Pipeline.registerFunction( contractionTrimmer, 'contractionTrimmer' );
|
||||||
this.pipeline.after(lunr.trimmer, contractionTrimmer);
|
this.pipeline.after( lunr.trimmer, contractionTrimmer );
|
||||||
|
|
||||||
Object.keys(textData).forEach((c) => this.add(textData[c]));
|
Object.keys( textData ).forEach( c => this.add( textData[ c ] ) );
|
||||||
});
|
} );
|
||||||
|
|
||||||
const headings = getHeadingPropertyNames();
|
const headings = getHeadingPropertyNames();
|
||||||
headings.nameText = "Company name";
|
headings.nameText = 'Company name';
|
||||||
headings.websiteText = "Website";
|
headings.websiteText = 'Website';
|
||||||
headings.shortRegion = "Region";
|
headings.shortRegion = 'Region';
|
||||||
|
|
||||||
return { index, textData, headings };
|
return { index, textData, headings };
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue