mirror of
https://github.com/remoteintech/remote-jobs
synced 2024-12-26 04:13:10 +00:00
Add full profile search (#763)
* Prevent duplicate company names
* Fix output indentation
* Search full profile content using lunr.js
* Remove extra stop words
This wasn't really working correctly - the stop word 'work' would leave
instances of 'working' and 'works' in the index for example.
* Change company name description from "Name" to "Company name"
* Pre-process query:
- Search for terms in AND mode, per
https://lunrjs.com/guides/searching.html#term-presence
- Discard non-alphanumeric characters from the search
- Better handling of contractions and searching for stop words
* Display search query and results in the console
* Add special search token: _incomplete
* Add a link to search for incomplete profiles
* Revert "Add a link to search for incomplete profiles"
This reverts commit f6384c90cb
.
* Add link to search documentation
* Improve search explanation appearance when it spans multiple lines
* Fix searching for contractions
Previously, searching for e.g. "don't" wasn't working correctly. After
trimming the contraction, "do" is a stop word, so it should be ignored.
* Improve "empty search" message
* Prefer matches other than "company name" in search excerpts
* Move inline scripts before external scripts
This probably doesn't matter right now due to the way the scripts are
currently structured, but it might matter one day and it's more logical
this way.
* Fix search engine index progress
* Improve script indentation
* I got 99 problems and they're all bots
* Update script exit code
When a Node.js error occurs the exit code is probably going to be 1, so
we should use a different code.
* Fix the tests
* Update documentation
This was wrong (out of date), but the correct version is obvious from
reading the code.
* Make download progress work in both Chrome and Firefox
See https://stackoverflow.com/a/32799706
This commit is contained in:
parent
bbdf527608
commit
dac8b04fc8
18 changed files with 629 additions and 125 deletions
|
@ -9,7 +9,11 @@ const phin = require( 'phin' );
|
|||
const rimraf = require( 'rimraf' );
|
||||
const swig = require( 'swig-templates' );
|
||||
|
||||
const { parseFromDirectory, headingPropertyNames } = require( '../lib' );
|
||||
const {
|
||||
parseFromDirectory,
|
||||
headingPropertyNames,
|
||||
buildSearchData,
|
||||
} = require( '../lib' );
|
||||
const contentPath = path.join( __dirname, '..' );
|
||||
const sitePath = path.join( __dirname, '..', 'site' );
|
||||
const siteBuildPath = path.join( sitePath, 'build' );
|
||||
|
@ -211,7 +215,7 @@ async function buildSite() {
|
|||
|
||||
// Set up styles/scripts for specific pages
|
||||
const indexScripts = [ {
|
||||
url: '//cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.min.js',
|
||||
url: '//cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.7/lunr.min.js',
|
||||
}, {
|
||||
url: copyAssetToBuild( 'companies-table.js' ),
|
||||
} ];
|
||||
|
@ -223,6 +227,14 @@ async function buildSite() {
|
|||
fs.copyFileSync( path.join( faviconPath, f ), path.join( siteBuildPath, f ) );
|
||||
} );
|
||||
|
||||
// Generate search index
|
||||
console.log( 'Generating search index' );
|
||||
const searchIndexData = JSON.stringify( buildSearchData( data ) );
|
||||
const searchIndexFilename = copyAssetToBuild(
|
||||
'search.js',
|
||||
searchIndexData
|
||||
);
|
||||
|
||||
// Generate the index.html file from the main README
|
||||
// TODO: Build this page and its table dynamically; more filters
|
||||
const readmeTemplate = swig.compileFile(
|
||||
|
@ -232,6 +244,11 @@ async function buildSite() {
|
|||
writePage( 'index.html', readmeTemplate( {
|
||||
stylesheets,
|
||||
scripts: scripts.concat( indexScripts ),
|
||||
inlineScripts: [
|
||||
'\n\t\tvar searchIndexFilename = ' + JSON.stringify( searchIndexFilename ) + ';'
|
||||
+ '\n\t\tvar searchIndexSize = ' + JSON.stringify( searchIndexData.length ) + ';'
|
||||
+ '\n\t\t',
|
||||
],
|
||||
pageContent: data.readmeContent,
|
||||
editUrl: githubEditUrl( 'README.md' ),
|
||||
} ) );
|
||||
|
@ -249,6 +266,7 @@ async function buildSite() {
|
|||
writePage( path.join( dirname, 'index.html' ), companyTemplate( {
|
||||
stylesheets,
|
||||
scripts,
|
||||
inlineScripts: [],
|
||||
company,
|
||||
headingPropertyNames,
|
||||
missingHeadings,
|
||||
|
|
|
@ -47,4 +47,4 @@ console.log(
|
|||
( errorCount === 1 ? '' : 's' )
|
||||
);
|
||||
|
||||
process.exitCode = Math.min( errorCount, 99 );
|
||||
process.exitCode = ( errorCount > 0 ? 3 : 0 );
|
||||
|
|
102
lib/index.js
102
lib/index.js
|
@ -5,6 +5,7 @@ const path = require( 'path' );
|
|||
const util = require( 'util' );
|
||||
|
||||
const cheerio = require( 'cheerio' );
|
||||
const lunr = require( 'lunr' );
|
||||
const marked = require( 'marked' );
|
||||
|
||||
|
||||
|
@ -75,10 +76,13 @@ exports.stripExtraChars = stripExtraChars;
|
|||
/**
|
||||
* Other exports
|
||||
*/
|
||||
exports.headingPropertyNames = headingsAll.reduce( ( acc, val ) => {
|
||||
function getHeadingPropertyNames() {
|
||||
return headingsAll.reduce( ( acc, val ) => {
|
||||
acc[ toIdentifierCase( val ) ] = val;
|
||||
return acc;
|
||||
}, {} );
|
||||
}
|
||||
exports.headingPropertyNames = getHeadingPropertyNames();
|
||||
|
||||
|
||||
/**
|
||||
|
@ -88,6 +92,7 @@ exports.headingPropertyNames = headingsAll.reduce( ( acc, val ) => {
|
|||
* files, and validate and parse the content of the Markdown files.
|
||||
*/
|
||||
exports.parseFromDirectory = contentPath => {
|
||||
const companyNamesSeen = {};
|
||||
let errors = [];
|
||||
|
||||
function error( filename, msg, ...params ) {
|
||||
|
@ -137,13 +142,14 @@ exports.parseFromDirectory = contentPath => {
|
|||
let lastCompanyName = null;
|
||||
|
||||
$( 'tr' ).each( ( i, tr ) => {
|
||||
const $tr = $( tr );
|
||||
if ( i === 0 ) {
|
||||
// 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.
|
||||
return;
|
||||
}
|
||||
const $td = $( tr ).children( 'td' );
|
||||
const $td = $tr.children( 'td' );
|
||||
|
||||
const websiteUrl = $td.eq( 1 ).text();
|
||||
const websiteText = websiteUrl
|
||||
|
@ -161,10 +167,25 @@ exports.parseFromDirectory = contentPath => {
|
|||
shortRegion: $td.eq( 2 ).text().trim(),
|
||||
};
|
||||
|
||||
if ( ! readmeEntry.name ) {
|
||||
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, '' )
|
||||
$tr.html().replace( /\n/g, '' )
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -174,14 +195,14 @@ exports.parseFromDirectory = contentPath => {
|
|||
) {
|
||||
readmeError(
|
||||
'Invalid content in Website column: %s',
|
||||
$( tr ).html().replace( /\n/g, '' )
|
||||
$tr.html().replace( /\n/g, '' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $td.eq( 2 ).children().length > 0 ) {
|
||||
readmeError(
|
||||
'Extra content in Region column: %s',
|
||||
$( tr ).html().replace( /\n/g, '' )
|
||||
$tr.html().replace( /\n/g, '' )
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -235,7 +256,10 @@ exports.parseFromDirectory = contentPath => {
|
|||
);
|
||||
}
|
||||
|
||||
// Set classes on table cells
|
||||
// Set identifying attributes on table elements
|
||||
$tr
|
||||
.attr( 'class', 'company-row' )
|
||||
.attr( 'id', 'company-row-' + ( i - 1 ) );
|
||||
$td.eq( 0 ).attr( 'class', 'company-name' );
|
||||
$td.eq( 1 ).attr( 'class', 'company-website' );
|
||||
$td.eq( 2 ).attr( 'class', 'company-region' );
|
||||
|
@ -500,3 +524,65 @@ exports.parseFromDirectory = contentPath => {
|
|||
readmeContent,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build search index data from the result of parseFromDirectory().
|
||||
*/
|
||||
exports.buildSearchData = data => {
|
||||
const textData = [];
|
||||
|
||||
data.companies.forEach( ( company, i ) => {
|
||||
const thisTextData = {
|
||||
id: String( i ),
|
||||
nameText: company.name,
|
||||
websiteText: company.websiteText,
|
||||
};
|
||||
|
||||
if ( company.shortRegion ) {
|
||||
thisTextData.shortRegion = company.shortRegion;
|
||||
}
|
||||
|
||||
Object.keys( exports.headingPropertyNames ).forEach( h => {
|
||||
if ( company.profileContent[ h ] ) {
|
||||
const text = cheerio.load( company.profileContent[ h ] ).text()
|
||||
// Replace warning emoji with a searchable token
|
||||
.replace( /\u26a0/, '(_incomplete)' );
|
||||
thisTextData[ h ] = text;
|
||||
}
|
||||
} );
|
||||
|
||||
textData.push( thisTextData );
|
||||
} );
|
||||
|
||||
const index = lunr( function() {
|
||||
this.field( 'nameText' );
|
||||
this.field( 'websiteText' );
|
||||
this.field( 'shortRegion' );
|
||||
|
||||
Object.keys( exports.headingPropertyNames ).forEach( h => {
|
||||
this.field( h );
|
||||
} );
|
||||
|
||||
// https://github.com/olivernn/lunr.js/issues/25#issuecomment-623267494
|
||||
this.metadataWhitelist = ['position'];
|
||||
|
||||
// https://github.com/olivernn/lunr.js/issues/192#issuecomment-172915226
|
||||
// https://gist.github.com/olivernn/7cd496f8654a0246c53c
|
||||
function contractionTrimmer( token ) {
|
||||
return token.update( str => {
|
||||
return str.replace( /('m|'ve|n't|'d|'ll|'ve|'s|'re)$/, '' );
|
||||
} );
|
||||
}
|
||||
lunr.Pipeline.registerFunction( contractionTrimmer, 'contractionTrimmer' );
|
||||
this.pipeline.after( lunr.trimmer, contractionTrimmer );
|
||||
|
||||
Object.keys( textData ).forEach( c => this.add( textData[ c ] ) );
|
||||
} );
|
||||
|
||||
const headings = getHeadingPropertyNames();
|
||||
headings.nameText = 'Company name';
|
||||
headings.websiteText = 'Website';
|
||||
headings.shortRegion = 'Region';
|
||||
|
||||
return { index, textData, headings };
|
||||
};
|
||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -627,6 +627,11 @@
|
|||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
|
||||
},
|
||||
"lunr": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.7.tgz",
|
||||
"integrity": "sha512-HjFSiy0Y0qZoW5OA1I6qBi7OnsDdqQnaUr03jhorh30maQoaP+4lQCKklYE3Nq3WJMSUfuBl6N+bKY5wxCb9hw=="
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"lunr": "2.3.7",
|
||||
"marked": "^0.7.0",
|
||||
"phin": "^3.4.0",
|
||||
"rimraf": "^3.0.0",
|
||||
|
|
|
@ -1,52 +1,222 @@
|
|||
function setupFilters() {
|
||||
function setupSearch() {
|
||||
var table = document.querySelector( 'table#companies-table' );
|
||||
|
||||
var headerCells = table.querySelectorAll( 'thead tr th' );
|
||||
headerCells[ 0 ].innerHTML =
|
||||
'<button class="sort" data-sort="company-name">Name</button>';
|
||||
headerCells[ 1 ].innerHTML =
|
||||
'<button class="sort" data-sort="company-website">Website</button>';
|
||||
headerCells[ 2 ].innerHTML =
|
||||
'<button class="sort" data-sort="company-region">Region</button>';
|
||||
var searchInput = document.createElement( 'input' );
|
||||
searchInput.type = 'text';
|
||||
searchInput.placeholder = 'Search';
|
||||
searchInput.id = 'search-input';
|
||||
|
||||
var tbody = table.querySelector( 'tbody' );
|
||||
tbody.setAttribute( 'class', 'list' );
|
||||
|
||||
var filterInput = document.createElement( 'input' );
|
||||
filterInput.type = 'text';
|
||||
filterInput.placeholder = 'Filter Companies';
|
||||
filterInput.id = 'company-filter';
|
||||
filterInput.setAttribute( 'class', 'company-filter' );
|
||||
var searchStatus = document.createElement( 'span' );
|
||||
searchStatus.id = 'search-status';
|
||||
|
||||
var companiesHeading = document.querySelector( 'h2#companies' );
|
||||
companiesHeading.appendChild( filterInput );
|
||||
companiesHeading.appendChild( searchInput );
|
||||
companiesHeading.appendChild( searchStatus );
|
||||
|
||||
var filtersExplanation = document.createElement( 'p' );
|
||||
filtersExplanation.id = 'filters-explanation';
|
||||
filtersExplanation.innerHTML = (
|
||||
'Use the text box above to filter the list of companies, '
|
||||
+ 'or click a column heading to sort by that column.'
|
||||
var searchExplanation = document.createElement( 'p' );
|
||||
searchExplanation.id = 'search-explanation';
|
||||
searchExplanation.innerHTML = (
|
||||
'Use the text box above to search all of our company data. '
|
||||
+ ' <a href="https://blog.remoteintech.company/search-help/">More info</a>'
|
||||
);
|
||||
table.parentNode.insertBefore( filtersExplanation, table );
|
||||
table.parentNode.insertBefore( searchExplanation, table );
|
||||
|
||||
window.tableFilter = new List(
|
||||
'main', // element ID that contains everything
|
||||
{
|
||||
valueNames: [
|
||||
'company-name',
|
||||
'company-website',
|
||||
'company-region'
|
||||
],
|
||||
searchClass: 'company-filter',
|
||||
var searchLoading = false;
|
||||
var searchData = null;
|
||||
var searchIndex = null;
|
||||
var updateTimeout = null;
|
||||
|
||||
function updateSearch() {
|
||||
if ( ! searchData || searchLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var searchValue = searchInput.value
|
||||
.replace( /[^a-z0-9_']+/gi, ' ' )
|
||||
.trim()
|
||||
.split( ' ' )
|
||||
.map( function( term ) {
|
||||
term = term
|
||||
.replace( /('m|'ve|n't|'d|'ll|'ve|'s|'re)$/, '' )
|
||||
.replace( /'/g, '' );
|
||||
if ( ! lunr.stopWordFilter( term.toLowerCase() ) ) {
|
||||
return null;
|
||||
} else if ( term ) {
|
||||
return '+' + term;
|
||||
} else {
|
||||
return term;
|
||||
}
|
||||
} )
|
||||
.filter( Boolean )
|
||||
.join( ' ' );
|
||||
var allMatch = ! searchValue;
|
||||
var searchResults = searchValue ? searchIndex.search( searchValue ) : [];
|
||||
var searchDisplayValue = (
|
||||
searchValue === '+_incomplete'
|
||||
? 'Incomplete profile'
|
||||
: searchInput.value.trim()
|
||||
);
|
||||
if ( allMatch ) {
|
||||
searchStatus.innerHTML = (
|
||||
'Empty search; showing all '
|
||||
+ searchData.textData.length
|
||||
+ ' companies'
|
||||
);
|
||||
} else if ( searchResults.length === 1 ) {
|
||||
searchStatus.innerText = searchDisplayValue + ': 1 result';
|
||||
} else {
|
||||
searchStatus.innerText = (
|
||||
searchDisplayValue + ': '
|
||||
+ searchResults.length + ' results'
|
||||
);
|
||||
}
|
||||
var searchMatches = {};
|
||||
searchResults.forEach( function( r ) {
|
||||
searchMatches[ +r.ref ] = r;
|
||||
} );
|
||||
if ( window.console && console.log ) {
|
||||
console.log( 'search', { value: searchValue, results: searchResults } );
|
||||
}
|
||||
searchData.textData.forEach( function( company, index ) {
|
||||
var match = searchMatches[ index ];
|
||||
var row = document.getElementById( 'company-row-' + index );
|
||||
var rowMatch = row.nextElementSibling;
|
||||
if ( rowMatch && rowMatch.classList.contains( 'company-match' ) ) {
|
||||
rowMatch.parentNode.removeChild( rowMatch );
|
||||
}
|
||||
row.style.display = ( match || allMatch ? '' : 'none' );
|
||||
row.classList.remove( 'has-match' );
|
||||
if ( match ) {
|
||||
row.classList.add( 'has-match' );
|
||||
var metadata = match.matchData.metadata;
|
||||
var contextWords = ( window.innerWidth <= 600 ? 4 : 6 );
|
||||
var k1, k2, pos;
|
||||
loop1: for ( k1 in metadata ) {
|
||||
for ( k2 in metadata[ k1 ] ) {
|
||||
pos = metadata[ k1 ][ k2 ].position[ 0 ];
|
||||
if ( k2 !== 'nameText' ) {
|
||||
// Accept company name for matches, but prefer
|
||||
// other fields if there are any
|
||||
break loop1;
|
||||
}
|
||||
}
|
||||
}
|
||||
rowMatch = document.createElement( 'tr' );
|
||||
rowMatch.setAttribute( 'class', 'company-match' );
|
||||
var rowMatchCell = document.createElement( 'td' );
|
||||
rowMatchCell.setAttribute( 'colspan', 3 );
|
||||
var spanBefore = document.createElement( 'span' );
|
||||
var spanMatch = document.createElement( 'strong' );
|
||||
var spanAfter = document.createElement( 'span' );
|
||||
var text = company[ k2 ];
|
||||
var words = [];
|
||||
var currentWord = '';
|
||||
var i, inWord, c;
|
||||
for ( i = pos[ 0 ] - 1; i >= 0; i-- ) {
|
||||
c = text.substring( i, i + 1 );
|
||||
inWord = /\S/.test( c );
|
||||
if ( inWord ) {
|
||||
currentWord = c + currentWord;
|
||||
}
|
||||
if ( ( ! inWord || i === 0 ) && currentWord ) {
|
||||
words.unshift( currentWord );
|
||||
currentWord = '';
|
||||
if ( words.length === contextWords + 1 ) {
|
||||
words[ 0 ] = '\u2026';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
spanBefore.innerText = (
|
||||
( window.innerWidth > 600 ? searchData.headings[ k2 ] + ': ' : '' )
|
||||
+ words.join( ' ' )
|
||||
+ ' '
|
||||
).replace( /\(_incomplete\)/, '(Incomplete)' );
|
||||
spanMatch.innerText = text
|
||||
.substring( pos[ 0 ], pos[ 0 ] + pos[ 1 ] )
|
||||
.replace( /\(_incomplete\)/, '(Incomplete)' );
|
||||
words = [];
|
||||
currentWord = '';
|
||||
for ( i = pos[ 0 ] + pos[ 1 ] + 1; i < text.length; i++ ) {
|
||||
c = text.substring( i, i + 1 );
|
||||
inWord = /\S/.test( c );
|
||||
if ( inWord ) {
|
||||
currentWord += c;
|
||||
}
|
||||
if ( ( ! inWord || i === text.length - 1 ) && currentWord ) {
|
||||
words.push( currentWord );
|
||||
currentWord = '';
|
||||
if ( words.length === contextWords + 1 ) {
|
||||
words[ contextWords ] = '\u2026';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
spanAfter.innerText = (
|
||||
' ' + words.join( ' ' )
|
||||
).replace( /\(_incomplete\)/, '(Incomplete)' );
|
||||
rowMatchCell.appendChild( spanBefore );
|
||||
rowMatchCell.appendChild( spanMatch );
|
||||
rowMatchCell.appendChild( spanAfter );
|
||||
rowMatch.appendChild( rowMatchCell );
|
||||
row.parentNode.insertBefore( rowMatch, row.nextSibling );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
searchInput.addEventListener( 'focus', function() {
|
||||
if ( searchData || searchLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchLoading = true;
|
||||
var searchLoadingText = 'Loading search data...';
|
||||
|
||||
searchStatus.innerHTML = searchLoadingText;
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open( 'GET', searchIndexFilename );
|
||||
|
||||
xhr.onprogress = function( e ) {
|
||||
var percentLoaded;
|
||||
if ( e.lengthComputable ) {
|
||||
percentLoaded = Math.round( 100 * e.loaded / e.total );
|
||||
} else {
|
||||
percentLoaded = Math.min(
|
||||
100,
|
||||
Math.round( 100 * e.loaded / searchIndexSize )
|
||||
);
|
||||
}
|
||||
searchStatus.innerHTML = searchLoadingText + ' ' + percentLoaded + '%';
|
||||
};
|
||||
|
||||
xhr.onload = function() {
|
||||
searchLoading = false;
|
||||
if ( xhr.status === 200 ) {
|
||||
searchData = JSON.parse( xhr.response );
|
||||
searchIndex = lunr.Index.load( searchData.index );
|
||||
updateSearch();
|
||||
} else {
|
||||
searchStatus.innerHTML = 'Error!';
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
} );
|
||||
|
||||
searchInput.addEventListener( 'keyup', function() {
|
||||
if ( updateTimeout ) {
|
||||
clearTimeout( updateTimeout );
|
||||
}
|
||||
updateTimeout = setTimeout( updateSearch, 450 );
|
||||
} );
|
||||
|
||||
document.body.setAttribute(
|
||||
'class',
|
||||
document.body.getAttribute( 'class' ) + ' filters-enabled'
|
||||
document.body.getAttribute( 'class' ) + ' search-enabled'
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener( 'DOMContentLoaded', function( event ) {
|
||||
setupFilters();
|
||||
setupSearch();
|
||||
} );
|
||||
|
|
|
@ -46,10 +46,10 @@ h1.company-name {
|
|||
}
|
||||
|
||||
/**
|
||||
* Styles for the companies table and filters on the main page
|
||||
* Styles for the companies table and search on the main page
|
||||
*/
|
||||
|
||||
#company-filter {
|
||||
#search-input {
|
||||
margin: 0 0 0 16px;
|
||||
padding: 8px;
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
|
@ -58,60 +58,47 @@ h1.company-name {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#filters-explanation {
|
||||
#search-status {
|
||||
margin-left: 18px;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#search-explanation {
|
||||
font-style: italic;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 50em) {
|
||||
#filters-explanation {
|
||||
#search-explanation {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
body.filters-enabled table#companies-table th {
|
||||
padding: 0;
|
||||
table#companies-table th {
|
||||
border-bottom: 2px solid #eee;
|
||||
line-height: 1;
|
||||
padding: 9px 6px;
|
||||
}
|
||||
|
||||
table#companies-table button.sort {
|
||||
width: 100%;
|
||||
table#companies-table td {
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 7px 3px 4px;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
color: #666;
|
||||
outline: none;
|
||||
}
|
||||
table#companies-table button.sort:hover,
|
||||
table#companies-table button.sort:focus {
|
||||
color: #c61610;
|
||||
}
|
||||
table#companies-table button.sort:hover {
|
||||
background: #f4f4f4;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Sort indicators adapted from http://listjs.com/examples/table/ */
|
||||
|
||||
table#companies-table .sort.asc:after,
|
||||
table#companies-table .sort.desc:after {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: 3px;
|
||||
top: -2px;
|
||||
table#companies-table tr.company-row td {
|
||||
border-top: 1px solid #eee;
|
||||
padding: 9px 6px;
|
||||
}
|
||||
table#companies-table tr.company-row.has-match td {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
table#companies-table .sort.asc:after {
|
||||
border-bottom: 6px solid;
|
||||
}
|
||||
|
||||
table#companies-table .sort.desc:after {
|
||||
border-top: 6px solid;
|
||||
table#companies-table tr.company-match td {
|
||||
padding: 0 0 9px 18px;
|
||||
font-size: 81%;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Column-specific styles */
|
||||
|
@ -132,16 +119,21 @@ table#companies-table td.company-region {
|
|||
/* Mobile-friendly display */
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
body.filters-enabled h2#companies {
|
||||
margin-bottom: 18px;
|
||||
body.search-enabled h2#companies {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#company-filter {
|
||||
#search-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 27px 0 0 0;
|
||||
}
|
||||
|
||||
#search-status {
|
||||
font-size: 13.5px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
table#companies-table,
|
||||
table#companies-table thead,
|
||||
table#companies-table tbody,
|
||||
|
@ -149,33 +141,31 @@ table#companies-table td.company-region {
|
|||
display: block;
|
||||
}
|
||||
|
||||
table#companies-table tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 0 0 8px 12px;
|
||||
table#companies-table tr.company-row {
|
||||
border-top: 1px solid #eee;
|
||||
padding: 7.5px 0 6px 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
table#companies-table tr.company-row.has-match {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
table#companies-table button.sort {
|
||||
width: auto;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
table#companies-table .sort.asc:after,
|
||||
table#companies-table .sort.desc:after {
|
||||
margin-right: 2px;
|
||||
table#companies-table tr.company-match td {
|
||||
padding-left: 12px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
table#companies-table thead tr {
|
||||
border-bottom-width: 3px;
|
||||
border-bottom: 2px solid #eee;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table#companies-table th,
|
||||
table#companies-table td {
|
||||
table#companies-table tr.company-row td,
|
||||
table#companies-table tr.company-row.has-match td {
|
||||
width: auto !important;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border-bottom-width: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
table#companies-table th {
|
||||
|
@ -191,11 +181,12 @@ table#companies-table td.company-region {
|
|||
display: none;
|
||||
}
|
||||
|
||||
table#companies-table td.company-name {
|
||||
table#companies-table tr.company-row td.company-name,
|
||||
table#companies-table tr.company-row.has-match td.company-name {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 0;
|
||||
padding-bottom: 3px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
{%- for stylesheet in stylesheets %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ stylesheet.url }}" />
|
||||
{%- endfor %}
|
||||
{%- for src in inlineScripts %}
|
||||
<script type="text/javascript">{{ src|safe }}</script>
|
||||
{%- endfor %}
|
||||
{%- for script in scripts %}
|
||||
<script type="text/javascript" src="{{ script.url }}"></script>
|
||||
{%- endfor %}
|
||||
|
|
13
test/fixtures/duplicate-company/README.md
vendored
Normal file
13
test/fixtures/duplicate-company/README.md
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Test data
|
||||
|
||||
This company table and its linked company profiles contain fully valid data.
|
||||
|
||||
## Companies
|
||||
|
||||
Name | Website | Region
|
||||
------------ | ------- | -------
|
||||
[&yet](/company-profiles/and-yet.md) | https://andyet.com | Worldwide
|
||||
[&Yet](/company-profiles/and.md) | https://andyet.com | Worldwide
|
||||
[10up](/company-profiles/10up.md) | https://10up.com/ | Worldwide
|
||||
[17hats](/company-profiles/17hats.md) | https://www.17hats.com/ | Worldwide
|
||||
[18F](/company-profiles/18f.md) | https://18f.gsa.gov/ | USA
|
39
test/fixtures/duplicate-company/company-profiles/10up.md
vendored
Normal file
39
test/fixtures/duplicate-company/company-profiles/10up.md
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
# 10up, A WordPress Development Agency
|
||||
|
||||
## Company blurb
|
||||
|
||||
We make websites and content management simple and fun with premiere web design & development consulting services, by contributing to open platforms like WordPress, and by providing tools and products (like [PushUp](https://pushupnotifications.com/)) that make web publishing a cinch.
|
||||
|
||||
At 10up, we don’t just “make” things – we engineer them. We’re a group of people built to solve problems; made to create; wired to delight. From beautiful pixels to beautiful code, we constantly improve the things around us, applying our passions to our clients’ projects and goals.
|
||||
|
||||
We’ve had the privilege of working on big web projects for clients as diverse as TechCrunch, ESPN’s FiveThirtyEight and Grantland, SurveyMonkey, Junior Diabetes Research Foundation (JDRF), and Google.
|
||||
|
||||
## Company size
|
||||
|
||||
125 and growing, spread across web engineering, systems, design, project management, strategy/accounts, and operations.
|
||||
|
||||
## Remote status
|
||||
|
||||
10up didn’t integrate remote work – we intended to be remote from the start! Being remote allows us to find talent no matter where they're located, scale up to meet needs with relative fluidity, and have been bootstrapped from the start. We also recognize the challenges of working remotely, and put a lot of effort into in-person meetups, communication tools, and ensuring that employees have the benefits and support they need no matter where they are.
|
||||
|
||||
## Region
|
||||
|
||||
We have employees all around the world, from across the US to the UK to South Africa to the Philippines. Most are currently located in North America, a number travel frequently, and some even work nomadically.
|
||||
|
||||
## Company technologies
|
||||
|
||||
* WordPress
|
||||
* PHP
|
||||
* Sass
|
||||
* Git
|
||||
* Vagrant
|
||||
* Nginx
|
||||
* Memcache
|
||||
|
||||
## Office locations
|
||||
|
||||
None; or everywhere!
|
||||
|
||||
## How to apply
|
||||
|
||||
Check out our [careers page](https://10up.com/careers/) and send an email to jobs@10up.com. Our amazing Recruitment Manager Christine Garrison will be on the other end.
|
25
test/fixtures/duplicate-company/company-profiles/17hats.md
vendored
Normal file
25
test/fixtures/duplicate-company/company-profiles/17hats.md
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
# 17hats
|
||||
|
||||
## Company blurb
|
||||
|
||||
Are you ready to stop juggling multiple apps to manage your business? From booking clients, to managing projects, to controlling your finances, with 17hats you have everything you need to manage your business and clients anytime, anywhere. Pretty neat if we say so ourselves! We created 17hats specifically for “businesses of one” with a focus on simplicity and ease of use.
|
||||
|
||||
## Company size
|
||||
|
||||
11-50
|
||||
|
||||
## Remote status
|
||||
|
||||
Every engineer on our team works remotely. That being said, we do need someone who can work California business hours (9am-6pm PST) and who is available for emergencies at night. If you live in the LA area, you are welcome to be in our cool office in Pasadena. Bonus points if you can curse in Dutch.
|
||||
|
||||
## Region
|
||||
|
||||
Worldwide
|
||||
|
||||
## Company technologies
|
||||
|
||||
iOS, React, Knockout, Rails, Perl, HTML, Sql, Ruby, JQuery
|
||||
|
||||
## How to apply
|
||||
|
||||
Email buford@17hats.com with github and/or CV
|
55
test/fixtures/duplicate-company/company-profiles/18f.md
vendored
Normal file
55
test/fixtures/duplicate-company/company-profiles/18f.md
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
# 18F
|
||||
|
||||
## Company blurb
|
||||
|
||||
18F is a civic consultancy for the government, inside the government, working with agencies to rapidly deploy tools and services that are easy to use, cost efficient, and reusable. Our goal is to change how the government buys and develops digital services by helping agencies adopt modern techniques that deliver superior products.
|
||||
|
||||
We are transforming government from the inside out, creating cultural change by working with teams inside agencies who want to create great services for the public.
|
||||
|
||||
We are a trusted partner for agencies working to transform how they build and buy tools and services in a user-centered way.
|
||||
|
||||
We will accomplish our mission by:
|
||||
|
||||
putting the needs of the public first
|
||||
being design-centric, agile, open, and data-driven
|
||||
deploying tools and services early and often
|
||||
|
||||
## Company size
|
||||
|
||||
100+
|
||||
|
||||
## Remote status
|
||||
|
||||
18F employees live all over the country. We work out of homes in Dayton and Tucson, St. Louis and Chapel Hill — and in federal buildings in San Francisco, Chicago, New York City, and Washington D.C.
|
||||
|
||||
That means many of our project teams are also made up of distributed employees working all over the country. For example, you may have a developer in Austin, a designer in Washington D.C., and a content strategist in Portland — but they’re all working on the same team and with the same partners.
|
||||
|
||||
Because we work on distributed teams so frequently, we've developed certain strategies for working well as a collaborative operation.
|
||||
|
||||
[We have a “remote first” mindset.](https://18f.gsa.gov/2015/10/15/best-practices-for-distributed-teams/)
|
||||
|
||||
## Region
|
||||
|
||||
U.S. citizens, non-citizens who are nationals of the U.S., or people who have been admitted to the U.S. for permanent residence and hold a valid green card.
|
||||
|
||||
## Company technologies
|
||||
|
||||
Ruby, Python, HTML, CSS, JavaScript
|
||||
|
||||
## Office locations
|
||||
|
||||
Federal buildings in San Francisco, Chicago, New York City, and Washington D.C.
|
||||
|
||||
## How to apply
|
||||
|
||||
[Open positions](https://pages.18f.gov/joining-18f/open-positions/)
|
||||
|
||||
If you want to apply directly to 18F please email join18f@gsa.gov. We don’t require a formal cover letter, but let us know more about you:
|
||||
|
||||
Send your current resume, preferably as a PDF.
|
||||
Link to your GitHub profile, design portfolio, or attach a writing sample.
|
||||
Specify what role you’d like to be considered for. Check out our openings here.
|
||||
If you're a Veteran of the U.S. Armed Forces or if you are eligible for "derived" preference, please mention that in your email so we can give you priority consideration.
|
||||
Don't see an opening that suits you? Tell us what you want to do!
|
||||
|
||||
[How to apply](https://pages.18f.gov/joining-18f/how-to-apply/)
|
44
test/fixtures/duplicate-company/company-profiles/and-yet.md
vendored
Normal file
44
test/fixtures/duplicate-company/company-profiles/and-yet.md
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
# &yet (And Yet)
|
||||
|
||||
## Company blurb
|
||||
[&yet](https://andyet.com) is about people. We’re known as a design and development consultancy (specializing in Node, React, and realtime), but we don’t fit neatly in a box.
|
||||
|
||||
We design and [develop custom software](https://andyet.com/software) for web, mobile, desktop, chat, and voice.
|
||||
|
||||
We enable millions of people to make super simple video calls with [Talky](https://talky.io).
|
||||
|
||||
We pioneer software and standards for [realtime communications](https://andyet.com/realtime).
|
||||
|
||||
We [wrote the book](https://gatherthepeople.com) on taking a human approach to marketing for people who would rather make what they love than persuade people to buy it.
|
||||
|
||||
We create high-impact conference experiences such as [RealtimeConf](http://experience.realtimeconf.com) and more recently–[&yetConf](http://andyetconf.com).
|
||||
|
||||
[Learn more about our team](https://andyet.com/about).
|
||||
|
||||
## Company size
|
||||
20+
|
||||
|
||||
## Remote status
|
||||
We employ several strategies to ensure an inclusive and collaborative environment for all our employees.
|
||||
|
||||
To communicate we use [Slack](https://slack.com) (text-chat), our own product [Talky](https://talky.io) (video chat and meetings), [Twist](https://twistapp.com) (daily check-ins) and [GitHub](https://github.com) (organization wide discussions).
|
||||
|
||||
One-on-ones and bi-weekly company-wide updates are a crucial part of staying connected and understanding our team as things change. We encourage employees to use these meetings to bring up frustrations, ideas, or whatever they need in order to be their best selves and to do their best work.
|
||||
|
||||
At least once a year we organize an in-person all-hands team week. It’s the best.
|
||||
|
||||
## Region
|
||||
&yet has one office located in Richland, WA. Currently ten people are working remotely out of Seattle, Portland, Folsom, Phoenix, Denver, Kansas City, Frankfurt, Oslo, and Melbourne. The most significant timezone difference is 17 hours.
|
||||
|
||||
## Company technologies
|
||||
* Node.js
|
||||
* React
|
||||
* WebRTC
|
||||
* Pug
|
||||
* Stylus
|
||||
|
||||
## Office locations
|
||||
[Fuse Coworking in Richland, WA](https://goo.gl/maps/oJaAQFf12tv)
|
||||
|
||||
## How to apply
|
||||
No current openings.
|
44
test/fixtures/duplicate-company/company-profiles/and.md
vendored
Normal file
44
test/fixtures/duplicate-company/company-profiles/and.md
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
# &yet (And Yet)
|
||||
|
||||
## Company blurb
|
||||
[&yet](https://andyet.com) is about people. We’re known as a design and development consultancy (specializing in Node, React, and realtime), but we don’t fit neatly in a box.
|
||||
|
||||
We design and [develop custom software](https://andyet.com/software) for web, mobile, desktop, chat, and voice.
|
||||
|
||||
We enable millions of people to make super simple video calls with [Talky](https://talky.io).
|
||||
|
||||
We pioneer software and standards for [realtime communications](https://andyet.com/realtime).
|
||||
|
||||
We [wrote the book](https://gatherthepeople.com) on taking a human approach to marketing for people who would rather make what they love than persuade people to buy it.
|
||||
|
||||
We create high-impact conference experiences such as [RealtimeConf](http://experience.realtimeconf.com) and more recently–[&yetConf](http://andyetconf.com).
|
||||
|
||||
[Learn more about our team](https://andyet.com/about).
|
||||
|
||||
## Company size
|
||||
20+
|
||||
|
||||
## Remote status
|
||||
We employ several strategies to ensure an inclusive and collaborative environment for all our employees.
|
||||
|
||||
To communicate we use [Slack](https://slack.com) (text-chat), our own product [Talky](https://talky.io) (video chat and meetings), [Twist](https://twistapp.com) (daily check-ins) and [GitHub](https://github.com) (organization wide discussions).
|
||||
|
||||
One-on-ones and bi-weekly company-wide updates are a crucial part of staying connected and understanding our team as things change. We encourage employees to use these meetings to bring up frustrations, ideas, or whatever they need in order to be their best selves and to do their best work.
|
||||
|
||||
At least once a year we organize an in-person all-hands team week. It’s the best.
|
||||
|
||||
## Region
|
||||
&yet has one office located in Richland, WA. Currently ten people are working remotely out of Seattle, Portland, Folsom, Phoenix, Denver, Kansas City, Frankfurt, Oslo, and Melbourne. The most significant timezone difference is 17 hours.
|
||||
|
||||
## Company technologies
|
||||
* Node.js
|
||||
* React
|
||||
* WebRTC
|
||||
* Pug
|
||||
* Stylus
|
||||
|
||||
## Office locations
|
||||
[Fuse Coworking in Richland, WA](https://goo.gl/maps/oJaAQFf12tv)
|
||||
|
||||
## How to apply
|
||||
No current openings.
|
|
@ -12,37 +12,37 @@ website links.</p>
|
|||
<th>Region</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<tbody><tr class="company-row" id="company-row-0">
|
||||
<td class="company-name"><a href="/and-yet/">&yet</a></td>
|
||||
<td class="company-website"><a href="https://andyet.com" target="_blank" rel="noopener noreferrer">andyet.com</a></td>
|
||||
<td class="company-region">Worldwide</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="company-row" id="company-row-1">
|
||||
<td class="company-name"><a href="/10up/">10up</a></td>
|
||||
<td class="company-website"><a href="https://10up.com/" target="_blank" rel="noopener noreferrer">10up.com</a></td>
|
||||
<td class="company-region">Worldwide</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="company-row" id="company-row-2">
|
||||
<td class="company-name"><a href="/17hats/">17hats</a></td>
|
||||
<td class="company-website"><a href="https://www.17hats.com/" target="_blank" rel="noopener noreferrer">17hats.com</a></td>
|
||||
<td class="company-region">Worldwide</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="company-row" id="company-row-3">
|
||||
<td class="company-name"><a href="/18f/">18F</a></td>
|
||||
<td class="company-website"><a href="https://18f.gsa.gov/" target="_blank" rel="noopener noreferrer">18f.gsa.gov</a></td>
|
||||
<td class="company-region">USA</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="company-row" id="company-row-4">
|
||||
<td class="company-name"><a href="/45royale/">45royale</a> ⚠</td>
|
||||
<td class="company-website"><a href="http://45royale.com/" target="_blank" rel="noopener noreferrer">45royale.com</a></td>
|
||||
<td class="company-region"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="company-row" id="company-row-5">
|
||||
<td class="company-name"><a href="/aerolab/">Aerolab</a> ⚠</td>
|
||||
<td class="company-website"><a href="https://aerolab.co/" target="_blank" rel="noopener noreferrer">aerolab.co</a></td>
|
||||
<td class="company-region"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="company-row" id="company-row-6">
|
||||
<td class="company-name"><a href="/angularclass/">AngularClass</a> ⚠</td>
|
||||
<td class="company-website"><a href="http://www.wikihow.com/wikiHow:About-wikiHow" target="_blank" rel="noopener noreferrer">wikihow.com/wikiHow:About-wikiHow</a></td>
|
||||
<td class="company-region">PST Timezone</td>
|
||||
|
|
|
@ -54,9 +54,7 @@ exports.runValidationScriptWithFixtures = ( dirName, env = {} ) => {
|
|||
const output = result.stdout.toString().trim().split( '\n' );
|
||||
const exitCode = result.status;
|
||||
|
||||
expect( output[ output.length - 1 ] ).to.equal(
|
||||
exitCode + ' problem' + ( exitCode === 1 ? '' : 's' ) + ' detected'
|
||||
);
|
||||
let errorSummary = output[ output.length - 1 ];
|
||||
if ( output.length >= 2 ) {
|
||||
expect( output[ output.length - 2 ] ).to.equal( '' );
|
||||
output.splice( -2 );
|
||||
|
@ -70,5 +68,5 @@ exports.runValidationScriptWithFixtures = ( dirName, env = {} ) => {
|
|||
} );
|
||||
}
|
||||
|
||||
return { output, exitCode };
|
||||
return { output, errorSummary, exitCode };
|
||||
};
|
||||
|
|
|
@ -39,6 +39,15 @@ describe( 'validation errors', () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
it( 'should catch duplicate company names', () => {
|
||||
expectValidateFixturesResult( 'duplicate-company', {
|
||||
errorCount: 1,
|
||||
output: [
|
||||
'README.md: Duplicate company: &Yet',
|
||||
]
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should catch unsorted company names', () => {
|
||||
expectValidateFixturesResult( 'unsorted', {
|
||||
errorCount: 2,
|
||||
|
|
|
@ -6,6 +6,7 @@ describe( 'validation script (integration tests)', () => {
|
|||
it( 'should pass with valid data', () => {
|
||||
expect( runValidationScriptWithFixtures( 'valid' ) ).to.eql( {
|
||||
exitCode: 0,
|
||||
errorSummary: '0 problems detected',
|
||||
output: [],
|
||||
} );
|
||||
} );
|
||||
|
@ -14,6 +15,7 @@ describe( 'validation script (integration tests)', () => {
|
|||
const env = { REPORT_PROFILE_HEADINGS: 'y' };
|
||||
expect( runValidationScriptWithFixtures( 'valid-incomplete', env ) ).to.eql( {
|
||||
exitCode: 0,
|
||||
errorSummary: '0 problems detected',
|
||||
output: [
|
||||
'Profile headings by count (7 total profiles):',
|
||||
'Company blurb: 7',
|
||||
|
@ -30,7 +32,8 @@ describe( 'validation script (integration tests)', () => {
|
|||
it( 'should catch unsorted company names, and count headings', () => {
|
||||
const env = { REPORT_PROFILE_HEADINGS: 'y' };
|
||||
expect( runValidationScriptWithFixtures( 'unsorted', env ) ).to.eql( {
|
||||
exitCode: 2,
|
||||
exitCode: 3,
|
||||
errorSummary: '2 problems detected',
|
||||
output: [
|
||||
'README.md: Company is listed out of order: "17hats" (should be before "18F")',
|
||||
'README.md: Company is listed out of order: "&yet" (should be before "17hats")',
|
||||
|
|
Loading…
Reference in a new issue