mirror of
https://github.com/remoteintech/remote-jobs
synced 2024-12-26 04:13:10 +00:00
Parse profile content into a JSON+HTML data structure (#445)
* A few minor tweaks * Add toIdentifierCase function * Use toIdentifierCase function to make profile content more code-friendly * Improve "incomplete profile" indicator checks In order to properly check the "incomplete profile" indicators, the "Company blurb" section should exist. * Return parsed HTML content for each company * Test parsing of invisible "variation selector" character * Strip out invisible "variation selector" character
This commit is contained in:
parent
5cce269df4
commit
2682009c9f
33 changed files with 429 additions and 29 deletions
68
lib/index.js
68
lib/index.js
|
@ -23,6 +23,7 @@ const headingsOptional = [
|
|||
'Office locations',
|
||||
'How to apply',
|
||||
];
|
||||
const headingsAll = headingsRequired.concat( headingsOptional );
|
||||
|
||||
|
||||
/**
|
||||
|
@ -46,6 +47,30 @@ function jsonStringifyUnicodeEscaped( obj ) {
|
|||
}
|
||||
exports.jsonStringifyUnicodeEscaped = jsonStringifyUnicodeEscaped;
|
||||
|
||||
function toIdentifierCase( text ) {
|
||||
return text
|
||||
.replace( /'/g, '' )
|
||||
.replace( /[^a-z0-9]+/gi, ' ' )
|
||||
.trim()
|
||||
.split( /\s+/ )
|
||||
.map( ( word, i ) => {
|
||||
if ( i === 0 ) {
|
||||
return word.toLowerCase();
|
||||
}
|
||||
return (
|
||||
word.substr( 0, 1 ).toUpperCase()
|
||||
+ word.substr( 1 ).toLowerCase()
|
||||
);
|
||||
} )
|
||||
.join( '' );
|
||||
}
|
||||
exports.toIdentifierCase = toIdentifierCase;
|
||||
|
||||
function stripExtraChars( text ) {
|
||||
return text.replace( /\ufe0f/g, '' );
|
||||
}
|
||||
exports.stripExtraChars = stripExtraChars;
|
||||
|
||||
|
||||
/**
|
||||
* The main exported function
|
||||
|
@ -71,10 +96,10 @@ exports.parseFromDirectory = contentPath => {
|
|||
|
||||
const readmeCompanies = [];
|
||||
|
||||
const readmeMarkdown = fs.readFileSync(
|
||||
const readmeMarkdown = stripExtraChars( fs.readFileSync(
|
||||
path.join( contentPath, 'README.md' ),
|
||||
'utf8'
|
||||
);
|
||||
) );
|
||||
|
||||
const $ = cheerio.load( marked( readmeMarkdown ) );
|
||||
|
||||
|
@ -99,9 +124,8 @@ exports.parseFromDirectory = contentPath => {
|
|||
}
|
||||
|
||||
const readmeEntry = {
|
||||
// Strip out warning emoji indicating that this profile is incomplete,
|
||||
// and any following unicode chars
|
||||
name: $td.eq( 0 ).text().replace( /\u26a0\ufe0f*/, '' ).trim(),
|
||||
// Strip out warning emoji indicating that this profile is incomplete
|
||||
name: $td.eq( 0 ).text().replace( /\u26a0/, '' ).trim(),
|
||||
// Detect warning emoji next to company name
|
||||
isIncomplete: /\u26a0/.test( $td.eq( 0 ).text() ),
|
||||
website: $td.eq( 1 ).text(),
|
||||
|
@ -177,10 +201,10 @@ exports.parseFromDirectory = contentPath => {
|
|||
error( filename, msg, ...params );
|
||||
}
|
||||
|
||||
const profileMarkdown = fs.readFileSync(
|
||||
const profileMarkdown = stripExtraChars( fs.readFileSync(
|
||||
path.join( profilesPath, filename ),
|
||||
'utf8'
|
||||
);
|
||||
) );
|
||||
const $ = cheerio.load( marked( profileMarkdown ) );
|
||||
|
||||
let hasTitleError = false;
|
||||
|
@ -263,14 +287,11 @@ exports.parseFromDirectory = contentPath => {
|
|||
allProfileHeadings[ headingName ].push( filename );
|
||||
}
|
||||
|
||||
if (
|
||||
headingsRequired.indexOf( headingName ) === -1 &&
|
||||
headingsOptional.indexOf( headingName ) === -1
|
||||
) {
|
||||
if ( headingsAll.indexOf( headingName ) === -1 ) {
|
||||
profileError(
|
||||
'Invalid section: "%s". Expected one of: %s',
|
||||
headingName,
|
||||
JSON.stringify( headingsRequired.concat( headingsOptional ) )
|
||||
JSON.stringify( headingsAll )
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
@ -287,6 +308,9 @@ exports.parseFromDirectory = contentPath => {
|
|||
// Build and validate the content of each section in this profile.
|
||||
|
||||
const profileContent = {};
|
||||
if ( readmeEntry ) {
|
||||
readmeEntry.profileContent = profileContent;
|
||||
}
|
||||
let currentHeading = null;
|
||||
|
||||
$( 'body' ).children().each( ( i, el ) => {
|
||||
|
@ -300,9 +324,13 @@ exports.parseFromDirectory = contentPath => {
|
|||
currentHeading = $el.html();
|
||||
profileContent[ currentHeading ] = '';
|
||||
} else if ( currentHeading ) {
|
||||
// Note: This assumes that the only possible children of the
|
||||
// 'body' are block-level elements. I think this is correct,
|
||||
// because from what I've seen, any inline content is wrapped
|
||||
// in a <p>.
|
||||
profileContent[ currentHeading ] = (
|
||||
profileContent[ currentHeading ]
|
||||
+ '\n' + $.html( el )
|
||||
+ '\n\n' + $.html( el )
|
||||
).trim();
|
||||
} else {
|
||||
profileError(
|
||||
|
@ -318,13 +346,20 @@ exports.parseFromDirectory = contentPath => {
|
|||
.trim();
|
||||
if ( ! sectionText ) {
|
||||
profileError(
|
||||
'Empty section: "%s". Leave it out instead.',
|
||||
'Empty section: "%s". Fill it in or leave it out instead.',
|
||||
heading
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
if ( readmeEntry ) {
|
||||
// Rewrite profile content to use more code-friendly heading names.
|
||||
Object.keys( profileContent ).forEach( headingName => {
|
||||
const headingIdentifier = toIdentifierCase( headingName );
|
||||
profileContent[ headingIdentifier ] = profileContent[ headingName ];
|
||||
delete profileContent[ headingName ];
|
||||
} );
|
||||
|
||||
if ( readmeEntry && profileContent.companyBlurb ) {
|
||||
// Check for company profiles that were filled in, but the "incomplete"
|
||||
// mark was left in the readme, or vice versa.
|
||||
const isIncomplete = {
|
||||
|
@ -333,7 +368,7 @@ exports.parseFromDirectory = contentPath => {
|
|||
profileHeadings.length === 1 &&
|
||||
profileHeadings[ 0 ] === 'Company blurb'
|
||||
),
|
||||
content: /⚠/.test( profileContent[ 'Company blurb' ] ),
|
||||
content: /⚠/.test( profileContent.companyBlurb ),
|
||||
};
|
||||
const incompleteCount = Object.values( isIncomplete )
|
||||
.reduce( ( sum, v ) => sum + ( v ? 1 : 0 ), 0 );
|
||||
|
@ -391,5 +426,6 @@ exports.parseFromDirectory = contentPath => {
|
|||
ok: true,
|
||||
profileFilenames,
|
||||
profileHeadingCounts,
|
||||
companies: readmeCompanies,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -15,3 +15,4 @@ Name | Website | Region
|
|||
[45royale](/company-profiles/45royale.md) ⚠ | http://45royale.com/ |
|
||||
[Aerolab](/company-profiles/aerolab.md) ⚠ | https://aerolab.co/ |
|
||||
[AngularClass](/company-profiles/angularclass.md) ⚠️ | https://angularclass.com | PST Timezone
|
||||
[Anomali](/company-profiles/anomali.md) ⚠️️ | https://www.anomali.com/company/careers | United States
|
||||
|
|
5
test/fixtures/mismatched-incomplete-indicators/company-profiles/anomali.md
vendored
Normal file
5
test/fixtures/mismatched-incomplete-indicators/company-profiles/anomali.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Anomali
|
||||
|
||||
## Invalid section name
|
||||
|
||||
No other content here!
|
|
@ -9,3 +9,21 @@ If you know something we don't, help us fill it in! Here's how:
|
|||
- Read our [Contributing Guidelines](https://github.com/remoteintech/remote-jobs/blob/master/CONTRIBUTING.md)
|
||||
- Have a look at our [example company profile](https://github.com/remoteintech/remote-jobs/blob/master/company-profiles/example.md)
|
||||
- Follow the structure of the example profile and [send us a pull request with your changes to this file!](https://github.com/remoteintech/remote-jobs/edit/master/company-profiles/45royale.md)
|
||||
|
||||
Without
|
||||
[variation selector](https://codepoints.net/U+FE0F?lang=en)
|
||||
invisible character:
|
||||
|
||||
> ⚠ |
|
||||
|
||||
**With**
|
||||
[variation selector](https://codepoints.net/U+FE0F?lang=en)
|
||||
invisible character:
|
||||
|
||||
> ⚠️ |
|
||||
|
||||
**With**
|
||||
[variation selector](https://codepoints.net/U+FE0F?lang=en)
|
||||
invisible character (x2):
|
||||
|
||||
> ⚠️️ |
|
||||
|
|
5
test/fixtures/valid-incomplete/parsed-content/10up.companyBlurb.html
vendored
Normal file
5
test/fixtures/valid-incomplete/parsed-content/10up.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
<p>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 <a href="https://pushupnotifications.com/">PushUp</a>) that make web publishing a cinch.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/10up.companySize.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/10up.companySize.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>125 and growing, spread across web engineering, systems, design, project management, strategy/accounts, and operations.</p>
|
9
test/fixtures/valid-incomplete/parsed-content/10up.companyTechnologies.html
vendored
Normal file
9
test/fixtures/valid-incomplete/parsed-content/10up.companyTechnologies.html
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<ul>
|
||||
<li>WordPress</li>
|
||||
<li>PHP</li>
|
||||
<li>Sass</li>
|
||||
<li>Git</li>
|
||||
<li>Vagrant</li>
|
||||
<li>Nginx</li>
|
||||
<li>Memcache</li>
|
||||
</ul>
|
1
test/fixtures/valid-incomplete/parsed-content/10up.howToApply.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/10up.howToApply.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>Check out our <a href="https://10up.com/careers/">careers page</a> and send an email to jobs@10up.com. Our amazing Recruitment Manager Christine Garrison will be on the other end.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/10up.region.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/10up.region.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>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.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/10up.remoteStatus.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/10up.remoteStatus.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>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.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/17hats.companyBlurb.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/17hats.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>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.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/17hats.companySize.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/17hats.companySize.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>11-50</p>
|
1
test/fixtures/valid-incomplete/parsed-content/17hats.companyTechnologies.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/17hats.companyTechnologies.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>iOS, React, Knockout, Rails, Perl, HTML, Sql, Ruby, JQuery</p>
|
1
test/fixtures/valid-incomplete/parsed-content/17hats.region.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/17hats.region.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>Worldwide</p>
|
1
test/fixtures/valid-incomplete/parsed-content/17hats.remoteStatus.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/17hats.remoteStatus.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>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.</p>
|
11
test/fixtures/valid-incomplete/parsed-content/18f.companyBlurb.html
vendored
Normal file
11
test/fixtures/valid-incomplete/parsed-content/18f.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>We are a trusted partner for agencies working to transform how they build and buy tools and services in a user-centered way.</p>
|
||||
|
||||
<p>We will accomplish our mission by:</p>
|
||||
|
||||
<p>putting the needs of the public first
|
||||
being design-centric, agile, open, and data-driven
|
||||
deploying tools and services early and often</p>
|
1
test/fixtures/valid-incomplete/parsed-content/18f.companySize.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/18f.companySize.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>100+</p>
|
11
test/fixtures/valid-incomplete/parsed-content/18f.howToApply.html
vendored
Normal file
11
test/fixtures/valid-incomplete/parsed-content/18f.howToApply.html
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
<p><a href="https://pages.18f.gov/joining-18f/open-positions/">Open positions</a></p>
|
||||
|
||||
<p>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:</p>
|
||||
|
||||
<p>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!</p>
|
||||
|
||||
<p><a href="https://pages.18f.gov/joining-18f/how-to-apply/">How to apply</a></p>
|
1
test/fixtures/valid-incomplete/parsed-content/18f.region.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/18f.region.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>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.</p>
|
7
test/fixtures/valid-incomplete/parsed-content/18f.remoteStatus.html
vendored
Normal file
7
test/fixtures/valid-incomplete/parsed-content/18f.remoteStatus.html
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>Because we work on distributed teams so frequently, we've developed certain strategies for working well as a collaborative operation.</p>
|
||||
|
||||
<p><a href="https://18f.gsa.gov/2015/10/15/best-practices-for-distributed-teams/">We have a “remote first” mindset.</a></p>
|
33
test/fixtures/valid-incomplete/parsed-content/45royale.companyBlurb.html
vendored
Normal file
33
test/fixtures/valid-incomplete/parsed-content/45royale.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
<p>⚠ We don't have much information about this company yet!</p>
|
||||
|
||||
<p>If you know something we don't, help us fill it in! Here's how:</p>
|
||||
|
||||
<ul>
|
||||
<li>Read our <a href="https://github.com/remoteintech/remote-jobs/blob/master/CONTRIBUTING.md">Contributing Guidelines</a></li>
|
||||
<li>Have a look at our <a href="https://github.com/remoteintech/remote-jobs/blob/master/company-profiles/example.md">example company profile</a></li>
|
||||
<li>Follow the structure of the example profile and <a href="https://github.com/remoteintech/remote-jobs/edit/master/company-profiles/45royale.md">send us a pull request with your changes to this file!</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Without
|
||||
<a href="https://codepoints.net/U+FE0F?lang=en">variation selector</a>
|
||||
invisible character:</p>
|
||||
|
||||
<blockquote>
|
||||
<p>⚠ |</p>
|
||||
</blockquote>
|
||||
|
||||
<p><strong>With</strong>
|
||||
<a href="https://codepoints.net/U+FE0F?lang=en">variation selector</a>
|
||||
invisible character:</p>
|
||||
|
||||
<blockquote>
|
||||
<p>⚠ |</p>
|
||||
</blockquote>
|
||||
|
||||
<p><strong>With</strong>
|
||||
<a href="https://codepoints.net/U+FE0F?lang=en">variation selector</a>
|
||||
invisible character (x2):</p>
|
||||
|
||||
<blockquote>
|
||||
<p>⚠ |</p>
|
||||
</blockquote>
|
9
test/fixtures/valid-incomplete/parsed-content/aerolab.companyBlurb.html
vendored
Normal file
9
test/fixtures/valid-incomplete/parsed-content/aerolab.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<p>⚠ We don't have much information about this company yet!</p>
|
||||
|
||||
<p>If you know something we don't, help us fill it in! Here's how:</p>
|
||||
|
||||
<ul>
|
||||
<li>Read our <a href="https://github.com/remoteintech/remote-jobs/blob/master/CONTRIBUTING.md">Contributing Guidelines</a></li>
|
||||
<li>Have a look at our <a href="https://github.com/remoteintech/remote-jobs/blob/master/company-profiles/example.md">example company profile</a></li>
|
||||
<li>Follow the structure of the example profile and <a href="https://github.com/remoteintech/remote-jobs/edit/master/company-profiles/aerolab.md">send us a pull request with your changes to this file!</a></li>
|
||||
</ul>
|
13
test/fixtures/valid-incomplete/parsed-content/and-yet.companyBlurb.html
vendored
Normal file
13
test/fixtures/valid-incomplete/parsed-content/and-yet.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
<p><a href="https://andyet.com">&yet</a> 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.</p>
|
||||
|
||||
<p>We design and <a href="https://andyet.com/software">develop custom software</a> for web, mobile, desktop, chat, and voice.</p>
|
||||
|
||||
<p>We enable millions of people to make super simple video calls with <a href="https://talky.io">Talky</a>.</p>
|
||||
|
||||
<p>We pioneer software and standards for <a href="https://andyet.com/realtime">realtime communications</a>.</p>
|
||||
|
||||
<p>We <a href="https://gatherthepeople.com">wrote the book</a> on taking a human approach to marketing for people who would rather make what they love than persuade people to buy it.</p>
|
||||
|
||||
<p>We create high-impact conference experiences such as <a href="http://experience.realtimeconf.com">RealtimeConf</a> and more recently–<a href="http://andyetconf.com">&yetConf</a>.</p>
|
||||
|
||||
<p><a href="https://andyet.com/about">Learn more about our team</a>.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/and-yet.companySize.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/and-yet.companySize.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>20+</p>
|
7
test/fixtures/valid-incomplete/parsed-content/and-yet.companyTechnologies.html
vendored
Normal file
7
test/fixtures/valid-incomplete/parsed-content/and-yet.companyTechnologies.html
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
<ul>
|
||||
<li>Node.js</li>
|
||||
<li>React</li>
|
||||
<li>WebRTC</li>
|
||||
<li>Pug</li>
|
||||
<li>Stylus</li>
|
||||
</ul>
|
1
test/fixtures/valid-incomplete/parsed-content/and-yet.howToApply.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/and-yet.howToApply.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>No current openings.</p>
|
1
test/fixtures/valid-incomplete/parsed-content/and-yet.officeLocations.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/and-yet.officeLocations.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p><a href="https://goo.gl/maps/oJaAQFf12tv">Fuse Coworking in Richland, WA</a></p>
|
1
test/fixtures/valid-incomplete/parsed-content/and-yet.region.html
vendored
Normal file
1
test/fixtures/valid-incomplete/parsed-content/and-yet.region.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<p>&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.</p>
|
7
test/fixtures/valid-incomplete/parsed-content/and-yet.remoteStatus.html
vendored
Normal file
7
test/fixtures/valid-incomplete/parsed-content/and-yet.remoteStatus.html
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
<p>We employ several strategies to ensure an inclusive and collaborative environment for all our employees.</p>
|
||||
|
||||
<p>To communicate we use <a href="https://slack.com">Slack</a> (text-chat), our own product <a href="https://talky.io">Talky</a> (video chat and meetings), <a href="https://twistapp.com">Twist</a> (daily check-ins) and <a href="https://github.com">GitHub</a> (organization wide discussions).</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>At least once a year we organize an in-person all-hands team week. It’s the best.</p>
|
9
test/fixtures/valid-incomplete/parsed-content/angularclass.companyBlurb.html
vendored
Normal file
9
test/fixtures/valid-incomplete/parsed-content/angularclass.companyBlurb.html
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<p>⚠ We don't have much information about this company yet!</p>
|
||||
|
||||
<p>If you know something we don't, help us fill it in! Here's how:</p>
|
||||
|
||||
<ul>
|
||||
<li>Read our <a href="https://github.com/remoteintech/remote-jobs/blob/master/CONTRIBUTING.md">Contributing Guidelines</a></li>
|
||||
<li>Have a look at our <a href="https://github.com/remoteintech/remote-jobs/blob/master/company-profiles/example.md">example company profile</a></li>
|
||||
<li>Follow the structure of the example profile and <a href="https://github.com/remoteintech/remote-jobs/edit/master/company-profiles/angularclass.md">send us a pull request with your changes to this file!</a></li>
|
||||
</ul>
|
|
@ -3,6 +3,8 @@ const { assert } = require( 'chai' );
|
|||
const {
|
||||
companyNameToProfileFilename,
|
||||
jsonStringifyUnicodeEscaped,
|
||||
toIdentifierCase,
|
||||
stripExtraChars,
|
||||
} = require( '../lib' );
|
||||
|
||||
describe( 'companyNameToProfileFilename function', () => {
|
||||
|
@ -65,3 +67,68 @@ describe( 'jsonStringifyUnicodeEscaped function', () => {
|
|||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'toIdentifierCase function', () => {
|
||||
it( 'should convert all valid headings to the correct identifiers', () => {
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Company blurb' ),
|
||||
'companyBlurb'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Company size' ),
|
||||
'companySize'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Remote status' ),
|
||||
'remoteStatus'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Region' ),
|
||||
'region'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Company technologies' ),
|
||||
'companyTechnologies'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Office locations' ),
|
||||
'officeLocations'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'How to apply' ),
|
||||
'howToApply'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should behave reasonably for other input values', () => {
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( ' With Whitespace ' ),
|
||||
'withWhitespace'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'with <extra> chars 123.' ),
|
||||
'withExtraChars123'
|
||||
);
|
||||
assert.strictEqual(
|
||||
toIdentifierCase( 'Let\'s Encrypt' ),
|
||||
'letsEncrypt'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'stripExtraChars function', () => {
|
||||
it( 'should strip unwanted invisible characters', () => {
|
||||
assert.strictEqual(
|
||||
stripExtraChars( 'abc\ufe0f def' ),
|
||||
'abc def'
|
||||
);
|
||||
assert.strictEqual(
|
||||
stripExtraChars( '\u26a0\ufe0f' ),
|
||||
'\u26a0'
|
||||
);
|
||||
assert.strictEqual(
|
||||
stripExtraChars( '\u26a0\ufe0f\ufe0f \u26a0\ufe0f\ufe0f' ),
|
||||
'\u26a0 \u26a0'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
|
155
test/parsing.js
155
test/parsing.js
|
@ -1,30 +1,166 @@
|
|||
const fs = require( 'fs' );
|
||||
const path = require( 'path' );
|
||||
|
||||
const { expect } = require( 'chai' );
|
||||
|
||||
const { parseFixtures } = require( './lib' );
|
||||
|
||||
describe( 'content parsing and metadata', () => {
|
||||
it( 'should return content metadata for valid data', () => {
|
||||
expect( parseFixtures( 'valid' ) ).to.eql( {
|
||||
it( 'should return content and metadata for valid data', () => {
|
||||
function getContentFilename( profile, section ) {
|
||||
return path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'valid-incomplete',
|
||||
'parsed-content',
|
||||
profile + '.' + section + '.html'
|
||||
);
|
||||
}
|
||||
|
||||
function readContentFile( profile, section ) {
|
||||
const content = fs.readFileSync(
|
||||
getContentFilename( profile, section ),
|
||||
'utf8'
|
||||
);
|
||||
if ( content.substring( content.length - 1 ) === '\n' ) {
|
||||
return content.substring( 0, content.length - 1 );
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function getContent( profile, sections ) {
|
||||
return sections.reduce( ( content, section ) => {
|
||||
content[ section ] = readContentFile( profile, section );
|
||||
return content;
|
||||
}, {} );
|
||||
}
|
||||
|
||||
const result = parseFixtures( 'valid-incomplete' );
|
||||
expect( Object.keys( result ) ).to.eql(
|
||||
[ 'ok', 'profileFilenames', 'profileHeadingCounts', 'companies' ]
|
||||
);
|
||||
expect( result ).to.deep.include( {
|
||||
ok: true,
|
||||
profileFilenames: [
|
||||
'10up.md',
|
||||
'17hats.md',
|
||||
'18f.md',
|
||||
'and-yet.md'
|
||||
'45royale.md',
|
||||
'aerolab.md',
|
||||
'and-yet.md',
|
||||
'angularclass.md',
|
||||
],
|
||||
profileHeadingCounts: {
|
||||
'Company blurb': 4,
|
||||
'Company blurb': 7,
|
||||
'Company size': 4,
|
||||
'Remote status': 4,
|
||||
'Region': 4,
|
||||
'Company technologies': 4,
|
||||
'Office locations': 3,
|
||||
'How to apply': 4,
|
||||
'Company technologies': 3,
|
||||
'How to apply': 3,
|
||||
'Office locations': 1,
|
||||
},
|
||||
} );
|
||||
|
||||
if ( process.env.WRITE_PARSED_CONTENT ) {
|
||||
result.companies.forEach( company => {
|
||||
const profile = company.linkedFilename.replace( /\.md$/, '' );
|
||||
Object.keys( company.profileContent ).forEach( section => {
|
||||
fs.writeFileSync(
|
||||
getContentFilename( profile, section ),
|
||||
company.profileContent[ section ] + '\n'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
expect( result.companies ).to.eql( [
|
||||
{
|
||||
name: '&yet',
|
||||
isIncomplete: false,
|
||||
website: 'https://andyet.com',
|
||||
shortRegion: 'Worldwide',
|
||||
linkedFilename: 'and-yet.md',
|
||||
profileContent: getContent( 'and-yet', [
|
||||
'companyBlurb',
|
||||
'companySize',
|
||||
'remoteStatus',
|
||||
'region',
|
||||
'companyTechnologies',
|
||||
'officeLocations',
|
||||
'howToApply',
|
||||
] ),
|
||||
}, {
|
||||
name: '10up',
|
||||
isIncomplete: false,
|
||||
website: 'https://10up.com/',
|
||||
shortRegion: 'Worldwide',
|
||||
linkedFilename: '10up.md',
|
||||
profileContent: getContent( '10up', [
|
||||
'companyBlurb',
|
||||
'companySize',
|
||||
'remoteStatus',
|
||||
'region',
|
||||
'companyTechnologies',
|
||||
'howToApply',
|
||||
] ),
|
||||
}, {
|
||||
name: '17hats',
|
||||
isIncomplete: false,
|
||||
website: 'https://www.17hats.com/',
|
||||
shortRegion: 'Worldwide',
|
||||
linkedFilename: '17hats.md',
|
||||
profileContent: getContent( '17hats', [
|
||||
'companyBlurb',
|
||||
'companySize',
|
||||
'remoteStatus',
|
||||
'region',
|
||||
'companyTechnologies',
|
||||
] ),
|
||||
}, {
|
||||
name: '18F',
|
||||
isIncomplete: false,
|
||||
website: 'https://18f.gsa.gov/',
|
||||
shortRegion: 'USA',
|
||||
linkedFilename: '18f.md',
|
||||
profileContent: getContent( '18f', [
|
||||
'companyBlurb',
|
||||
'companySize',
|
||||
'remoteStatus',
|
||||
'region',
|
||||
'howToApply',
|
||||
] ),
|
||||
}, {
|
||||
name: '45royale',
|
||||
isIncomplete: true,
|
||||
website: 'http://45royale.com/',
|
||||
shortRegion: "",
|
||||
linkedFilename: '45royale.md',
|
||||
profileContent: getContent( '45royale', [
|
||||
'companyBlurb',
|
||||
] ),
|
||||
}, {
|
||||
name: 'Aerolab',
|
||||
isIncomplete: true,
|
||||
website: 'https://aerolab.co/',
|
||||
shortRegion: "",
|
||||
linkedFilename: 'aerolab.md',
|
||||
profileContent: getContent( 'aerolab', [
|
||||
'companyBlurb',
|
||||
] ),
|
||||
}, {
|
||||
name: 'AngularClass',
|
||||
isIncomplete: true,
|
||||
website: 'https://angularclass.com',
|
||||
shortRegion: 'PST Timezone',
|
||||
linkedFilename: 'angularclass.md',
|
||||
profileContent: getContent( 'angularclass', [
|
||||
'companyBlurb',
|
||||
] ),
|
||||
}
|
||||
] );
|
||||
} );
|
||||
|
||||
it( 'should return content metadata for unsorted company names', () => {
|
||||
it( 'should return errors and metadata for unsorted company names', () => {
|
||||
expect( parseFixtures( 'unsorted' ) ).to.eql( {
|
||||
ok: false,
|
||||
errors: [
|
||||
|
@ -54,7 +190,7 @@ describe( 'content parsing and metadata', () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
it( 'should return content metadata for orphaned company profiles', () => {
|
||||
it( 'should return errors and metadata for orphaned company profiles', () => {
|
||||
expect( parseFixtures( 'orphaned-profiles' ) ).to.eql( {
|
||||
ok: false,
|
||||
errors: [
|
||||
|
@ -81,4 +217,3 @@ describe( 'content parsing and metadata', () => {
|
|||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
|
|
@ -93,8 +93,8 @@ describe( 'validation errors', () => {
|
|||
'17hats.md: Invalid section: "A thing I made up". Expected one of: ["Company blurb","Company size","Remote status","Region","Company technologies","Office locations","How to apply"]',
|
||||
'17hats.md: Content is not part of any section: <p>Some extra content.</p>',
|
||||
'18f.md: Duplicate section: "Company size".',
|
||||
'18f.md: Empty section: "Region". Leave it out instead.',
|
||||
'18f.md: Empty section: "Remote status". Leave it out instead.',
|
||||
'18f.md: Empty section: "Region". Fill it in or leave it out instead.',
|
||||
'18f.md: Empty section: "Remote status". Fill it in or leave it out instead.',
|
||||
'1password.md: The main title is wrapped inside of another element.',
|
||||
'1password.md: The section heading for "Company size" is wrapped inside of another element.',
|
||||
'1password.md: Content is not part of any section: <blockquote><h1 id="1password">1Password</h1></blockquote>',
|
||||
|
@ -116,7 +116,7 @@ describe( 'validation errors', () => {
|
|||
|
||||
it( 'should catch mismatched "incomplete profile" indicators', () => {
|
||||
expectValidateFixturesResult( 'mismatched-incomplete-indicators', {
|
||||
errorCount: 7,
|
||||
errorCount: 9,
|
||||
output: [
|
||||
'10up.md: Profile is marked as complete, but it only contains a "Company blurb" heading.',
|
||||
'17hats.md: Profile looks complete, but the "Company blurb" contains a warning emoji.',
|
||||
|
@ -128,6 +128,8 @@ describe( 'validation errors', () => {
|
|||
'aerolab.md: Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.',
|
||||
'and-yet.md: Profile looks complete, but the main readme contains a warning emoji.',
|
||||
'angularclass.md: Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.',
|
||||
'anomali.md: Invalid section: "Invalid section name". Expected one of: ["Company blurb","Company size","Remote status","Region","Company technologies","Office locations","How to apply"]',
|
||||
'anomali.md: Required section "Company blurb" not found.',
|
||||
],
|
||||
} );
|
||||
} );
|
||||
|
|
Loading…
Reference in a new issue