From f46d31739bc5fbf3c2df58ebd35ab2f67f5e3c34 Mon Sep 17 00:00:00 2001 From: "kamil.dobrzynski" Date: Tue, 14 Dec 2021 09:59:02 +0100 Subject: [PATCH 01/25] feat: Add Uncapped company --- README.md | 1 + company-profiles/uncapped.md | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 company-profiles/uncapped.md diff --git a/README.md b/README.md index a2881670..db16dff6 100644 --- a/README.md +++ b/README.md @@ -587,6 +587,7 @@ Name | Website | Region [Twilio](/company-profiles/twilio.md) | https://www.twilio.com/ | Worldwide [Udacity](/company-profiles/udacity.md) | https://www.udacity.com/ | Worldwide [Uhuru](/company-profiles/uhuru.md) | https://uhurunetwork.com/ | Worldwide +[Uncapped](/company-profiles/uncapped.md) | https://weareuncapped.com/ | Europe, USA [Upwork Pro](/company-profiles/upwork-pro.md) | https://www.upwork.com | North America [Upworthy](/company-profiles/upworthy.md) | https://www.upworthy.com/ | Worldwide, Time Zone: PST, PDT [USAA](/company-profiles/usaa.md) | https://usaa.com | USA diff --git a/company-profiles/uncapped.md b/company-profiles/uncapped.md new file mode 100644 index 00000000..459878ee --- /dev/null +++ b/company-profiles/uncapped.md @@ -0,0 +1,57 @@ +# Uncapped + +## Company blurb + +Founded in 2019, Uncapped is the fastest, most affordable way for growing online businesses to fund marketing and inventory. The company was born out of frustration with the limited financing options available for UK and European entrepreneurs to finance growth. + +Uncapped provides business advances of between £10k and £5m without credit checks, personal guarantees, warrants, equity, or compounding interest and makes money by charging a low flat fee that is paid back from future sales revenue. + +Uncapped has raised $120 million from investors including Lakestar, Mouro Capital, Global Founders Capital, White Star Capital, Seedcamp, and All Iron Ventures. + +## Company size + +50-100 (Dec 2021) + +## Remote status + +Remote from day one + +## Region + +Europe, USA + +## Company technologies + +Backend: +- Java +- Spring Boot/Cloud +- PostgreSql +- Microservices + +Frontend: +- Typescript +- React + +Infrastructure: +- Docker +- Kubernetes (GKE) +- GCP +- Terraform + +Other tools +- Visual Studio Code +- Intellij Idea +- Slack +- GitLab +- FluxCD +- Jira +- Figma + + +## Office locations + +N/A + +## How to apply + +https://www.weareuncapped.com/careers From 5dad3f60dadd2be8714d4ecb78e7b7cd03f6d8c9 Mon Sep 17 00:00:00 2001 From: Kwuang Tang <10319942+cktang88@users.noreply.github.com> Date: Thu, 6 Jan 2022 23:34:46 -0600 Subject: [PATCH 02/25] Create ramp.md --- company-profiles/ramp.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 company-profiles/ramp.md diff --git a/company-profiles/ramp.md b/company-profiles/ramp.md new file mode 100644 index 00000000..7aedef23 --- /dev/null +++ b/company-profiles/ramp.md @@ -0,0 +1,35 @@ +# Ramp + +## Company blurb + +Founded in 2019 Ramp is building the world's first finance automation platform designed to save businesses time and money. We’re proud to be NYC’s fastest growing startup with a unicorn valuation of $3.9 billion. + +More than 2,000 businesses use Ramp, including fast-growing startups as well as established businesses across the US. Our investors, who have backed us to the tune of $620 million, include Founders Fund, Stripe, Goldman Sachs, Coatue Management, D1 Capital Partners, Redpoint Ventures and Thrive Capital, as well as over 100 angel investors from leading companies. + +Led by the same team that built, scaled, and sold Paribus to Capital One, we are part of the Forbes Fintech 50 and Fast Company's Best Workplaces for Innovators program. + +## Company size + +200+ + +## Remote status + +A remote-friendly culture with many positions that can be remote (although office visits a few times a year are encouraged). Most remote positions prefer a timezone within 4 hours of EST. + +## Region + +Worldwide, but US-focused. + +## Company technologies + +React, TypeScript, Python, PostgreSQL, Elixir. + +For remote communication, Slack and Zoom. + +## Office locations + +New York, Miami + +## How to apply + +Ramp openings are listed on the [careers](https://ramp.com/careers) page. From 13ffca8b46db09c1979e6b6e911cab71cd67166e Mon Sep 17 00:00:00 2001 From: Kwuang Tang <10319942+cktang88@users.noreply.github.com> Date: Thu, 6 Jan 2022 23:35:47 -0600 Subject: [PATCH 03/25] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a2881670..361fee4e 100644 --- a/README.md +++ b/README.md @@ -469,6 +469,7 @@ Name | Website | Region [Rackspace](/company-profiles/rackspace.md) | https://rackspace.com/ | Worldwide [Raft](/company-profiles/raft.md) | https://goraft.tech | USA [Rainforest QA](/company-profiles/rainforest-qa.md) | https://www.rainforestqa.com/jobs/ | Worldwide +[Ramp](/company-profiles/ramp.md) | https://www.ramp.com/ | Worldwide [Reaction Commerce](/company-profiles/reaction-commerce.md) | https://reactioncommerce.com/careers/ | Worldwide [ReactiveOps, Inc.](/company-profiles/reactiveops.md) | https://www.reactiveops.com | USA [real.digital](/company-profiles/real-digital.md) | https://www.real-digital.de|Europe UTC-1 to UTC+2 From 851a26324eb3af31e3dc9f50cbb0cc087c7b2dce Mon Sep 17 00:00:00 2001 From: fswang Date: Sun, 9 Jan 2022 20:42:52 +0800 Subject: [PATCH 04/25] Add new company - StreamNative --- README.md | 1 + company-profiles/streamnative.md | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 company-profiles/streamnative.md diff --git a/README.md b/README.md index a2881670..bfe76c88 100644 --- a/README.md +++ b/README.md @@ -536,6 +536,7 @@ Name | Website | Region [StoneCo](/company-profiles/stoneco.md) | https://www.stone.co/ | Brazil [StormX](/company-profiles/stormx.md) | https://stormx.io/ | Worldwide [Strapi](/company-profiles/strapi.md) | https://strapi.io/careers | Worldwide +[StreamNative](/company-profiles/streamnative.md) | https://streamnative.io/careers/ | Worldwide [Stripe](/company-profiles/stripe.md) | https://stripe.com/ | Worldwide [StudySoup](/company-profiles/studysoup.md) | https://studysoup.com/ | Worldwide [Superplayer & Co](/company-profiles/superplayer-and-co.md) | https://www.superplayer.co | Brazil, Latin America diff --git a/company-profiles/streamnative.md b/company-profiles/streamnative.md new file mode 100644 index 00000000..60dd1d4e --- /dev/null +++ b/company-profiles/streamnative.md @@ -0,0 +1,41 @@ + +# Example Company + +## Company blurb + +[StreamNative](https://streamnative.io/) is enabling organizations to build the next generation of messaging and event streaming applications. Leveraging Apache Pulsar and BookKeeper, we optimize for scalability and resiliency while reducing the overhead management and complexity required by incumbent technologies. We do this by offering Pulsar and StreamNative’s ‘products as a service’. + +StreamNative is building a world-class team that is passionate about building amazing products and committed to customer success. + + +## Company size + +50-100 employees worldwide. + +## Remote status + +The whole company are working remotely. Our teams are distributed. Some positions may have a region limit, but most (esp. engineering ones) are hiring globally. + +Our recruitment process and onboarding process are all remote. StreamNative will also be held employee exchange event regularly in every region to provide an opportunity for employees to match avatars to real faces and meet everyone. + +Teams heavily rely on different forms of asynchronous (Github, Google Spreadsheets/Docs, Google Groups, email) and synchronous (Slack, Zoom) communication where necessary and appropriate. + +## Region + +StreamNative is interested in hiring worldwide.Currently our employees are located in the following countries: + +* US +* China +* France +* Greece +* Ireland + +## Company technologies + +* Go +* C# +* Java + +## How to apply + +Apply at https://streamnative.io/careers/ From 2570199c2b33f2371d97f2a2b5f5b9c0f9faa6d8 Mon Sep 17 00:00:00 2001 From: fswang Date: Sun, 9 Jan 2022 20:46:35 +0800 Subject: [PATCH 05/25] Edit company profile name --- company-profiles/streamnative.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/company-profiles/streamnative.md b/company-profiles/streamnative.md index 60dd1d4e..48b807e1 100644 --- a/company-profiles/streamnative.md +++ b/company-profiles/streamnative.md @@ -1,5 +1,5 @@ -# Example Company +# Stream Native ## Company blurb From 238910709c439caf2e4cb50e6e4d613bdbd52c38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Feb 2022 23:40:09 +0000 Subject: [PATCH 06/25] Bump follow-redirects from 1.14.4 to 1.14.7 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.4 to 1.14.7. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.4...v1.14.7) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39572f48..77375ab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -581,9 +581,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", "dev": true, "funding": [ { @@ -2157,9 +2157,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", "dev": true }, "fs.realpath": { From 745db9c2912030e8862e92f790a3bd69f7904521 Mon Sep 17 00:00:00 2001 From: Sandy McFadden Date: Fri, 4 Feb 2022 21:17:01 -0400 Subject: [PATCH 07/25] Fix calls to marked to be marked.parse after a dependency update --- lib/index.js | 1200 +++++++++++++++++++++++++------------------------- 1 file changed, 597 insertions(+), 603 deletions(-) diff --git a/lib/index.js b/lib/index.js index ff887320..857694ec 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,661 +1,655 @@ #!/usr/bin/env node -const fs = require( 'fs' ); -const path = require( 'path' ); -const util = require( 'util' ); - -const cheerio = require( 'cheerio' ); -const lunr = require( 'lunr' ); -const marked = require( 'marked' ); +const fs = require("fs"); +const path = require("path"); +const util = require("util"); +const cheerio = require("cheerio"); +const lunr = require("lunr"); +const marked = require("marked"); /** * Constants */ -const headingsRequired = [ - 'Company blurb', -]; +const headingsRequired = ["Company blurb"]; const headingsOptional = [ - 'Company size', - 'Remote status', - 'Region', - 'Company technologies', - 'Office locations', - 'How to apply', + "Company size", + "Remote status", + "Region", + "Company technologies", + "Office locations", + "How to apply", ]; -const headingsAll = headingsRequired.concat( headingsOptional ); - +const headingsAll = headingsRequired.concat(headingsOptional); /** * Utility functions */ -function companyNameToProfileFilename( companyName ) { - return companyName.toLowerCase() - .replace( /&/g, ' and ' ) - .replace( /'/g, '' ) - .replace( /[^a-z0-9]+/gi, '-' ) - .replace( /^-|-$/g, '' ); +function companyNameToProfileFilename(companyName) { + return companyName + .toLowerCase() + .replace(/&/g, " and ") + .replace(/'/g, "") + .replace(/[^a-z0-9]+/gi, "-") + .replace(/^-|-$/g, ""); } exports.companyNameToProfileFilename = companyNameToProfileFilename; // adapted from https://gist.github.com/RandomEtc/2657669 -function jsonStringifyUnicodeEscaped( obj ) { - return JSON.stringify( obj ).replace( /[\u007f-\uffff]/g, c => { - return '\\u' + ( '0000' + c.charCodeAt( 0 ).toString( 16 ) ).slice( -4 ); - } ); +function jsonStringifyUnicodeEscaped(obj) { + return JSON.stringify(obj).replace(/[\u007f-\uffff]/g, (c) => { + return "\\u" + ("0000" + c.charCodeAt(0).toString(16)).slice(-4); + }); } 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( '' ); +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, '' ); +function stripExtraChars(text) { + return text.replace(/\ufe0f/g, ""); } exports.stripExtraChars = stripExtraChars; - /** * Other exports */ function getHeadingPropertyNames() { - return headingsAll.reduce( ( acc, val ) => { - acc[ toIdentifierCase( val ) ] = val; - return acc; - }, {} ); + return headingsAll.reduce((acc, val) => { + acc[toIdentifierCase(val)] = val; + return acc; + }, {}); } exports.headingPropertyNames = getHeadingPropertyNames(); - /** * The main exported function * * Start with a directory including a README.md and company-profiles/*.md * files, and validate and parse the content of the Markdown files. */ -exports.parseFromDirectory = contentPath => { - const companyNamesSeen = {}; - let errors = []; - - function error( filename, msg, ...params ) { - errors.push( { - filename, - message: util.format( msg, ...params ), - } ); - } - - // Build list of Markdown files containing company profiles. - const profilesPath = path.join( contentPath, 'company-profiles' ); - const profileFilenames = fs.readdirSync( profilesPath ); - - // Scan the company table in the readme. - - const readmeCompanies = []; - - const readmeMarkdown = stripExtraChars( fs.readFileSync( - path.join( contentPath, 'README.md' ), - 'utf8' - ) ); - - let inTable = false; - readmeMarkdown.split( '\n' ).forEach( line => { - if ( /^\s*-+\s*\|\s*-+\s*\|\s*-+\s*$/.test( line ) ) { - inTable = true; - } else if ( /^\s*$/.test( line ) ) { - inTable = false; - } else if ( inTable ) { - const fields = line.split( '|' ); - if ( fields.length !== 3 ) { - readmeError( - 'Expected 3 table cells but found %d: %s', - fields.length, - line - ); - } - } - } ); - - const $ = cheerio.load( marked( readmeMarkdown ) ); - - function readmeError( msg, ...params ) { - error( 'README.md', msg, ...params ); - } - - const mainUrl = 'remoteintech.company' - - function addTargetBlankAndExternalLinkIcons (el) { - if (el.type === 'tag') { - const anchorTagElements = el.children.filter(element => element.name === 'a') - if (anchorTagElements.length > 0) { - anchorTagElements.forEach(element => { - const url = element.attribs.href - const urlInfo = getUrlInfo(url) - - if (urlInfo.is_email || urlInfo.is_internal) { - return - } - - element.attribs.target = '_blank' - - $element = $( element ) - $element.append(' ') - }) - } - - if (el.children && el.children.length) { - el.children.forEach(element => { - addTargetBlankAndExternalLinkIcons(element) - }) - } - } - } - - /** - * Getting info about the url. It includes checking isEmail of isInternal - * @param {*} url - */ - function getUrlInfo (url) { - const data = {} - - if (url.match(/^mailto:/)) { // checking url email or not - data.is_email = true - return data - } - - const mainDomainFromGivenUrl = extractMainDomainFromUrl(url) - - // checking url is email or not - if (mainDomainFromGivenUrl !== mainUrl) { - data.is_internal = false - return data - } else { - data.is_internal = true - } - - return data - } - - /** - * Extracting main domain from the url - * @param {*} 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 data = domainRe.exec(url) - - const splittedDomain = data[2].split('.') - - if (splittedDomain.length === 2) { // check extra subdomain is present or not - return data[2] - } - - return splittedDomain[splittedDomain.length - 2] + '.' + splittedDomain[splittedDomain.length - 1] // return only main domain address - } - - 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' ); - // Skip the table header row. - return; - } - const $td = $tr.children( 'td' ); - - const websiteUrl = $td.eq( 1 ).text(); - const websiteText = websiteUrl - .replace( /^https?:\/\//, '' ) - .replace( /^www\./, '' ) - .replace( /\/$/, '' ); - - const readmeEntry = { - // 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() ), - websiteUrl, - websiteText, - shortRegion: $td.eq( 2 ).text().trim(), - }; - - 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( - '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, '' ) - ); - } - - if ( - lastCompanyName && - readmeEntry.name.toLowerCase() < lastCompanyName.toLowerCase() - ) { - readmeError( - 'Company is listed out of order: "%s" (should be before "%s")', - readmeEntry.name, - lastCompanyName - ); - } - lastCompanyName = readmeEntry.name; - - const $profileLink = $td.eq( 0 ).find( 'a' ); - - if ( $profileLink.length === 1 ) { - const match = $profileLink.attr( 'href' ).match( /^\/company-profiles\/(.*\.md)$/ ); - - if ( match ) { - readmeEntry.linkedFilename = match[ 1 ]; - if ( profileFilenames.indexOf( readmeEntry.linkedFilename ) === -1 ) { - readmeError( - 'Missing company profile for "%s", or broken link: "%s"', - readmeEntry.name, - $profileLink.attr( 'href' ) - ); - } - - const nameCheck = $profileLink.text().trim(); - if ( nameCheck !== readmeEntry.name ) { - readmeError( - 'Extra text in company name: %s, %s', - jsonStringifyUnicodeEscaped( nameCheck ), - jsonStringifyUnicodeEscaped( readmeEntry.name ) - ); - } - } else { - readmeError( - 'Invalid link to company profile for "%s": "%s"', - readmeEntry.name, - $profileLink.attr( 'href' ) - ); - } - } else { - readmeError( - 'Company "%s" has no linked Markdown profile ("/company-profiles/%s.md")', - readmeEntry.name, - companyNameToProfileFilename( readmeEntry.name ) - ); - } - - // 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' ); - - // Rewrite company profile link to the correct URL for the static site - if ( $profileLink.length ) { - $profileLink.attr( - 'href', - $profileLink.attr( 'href' ) - .replace( /^\/company-profiles\//, '/' ) - .replace( /\.md$/, '/' ) - ); - } - - // Rewrite external website link (target="_blank" etc, shorter text) - const $websiteLink = $td.eq( 1 ).children().eq( 0 ); - $websiteLink - .attr( 'target', '_blank' ) - .attr( 'rel', 'noopener noreferrer' ) - .text( websiteText ); - - readmeCompanies.push( readmeEntry ); - } ); - - const readmeContent = $( 'body' ).html(); - - // Scan the individual Markdown files containing the company profiles. - - const allProfileHeadings = {}; - - profileFilenames.forEach( filename => { - function profileError( msg, ...params ) { - error( filename, msg, ...params ); - } - - const profileMarkdown = stripExtraChars( fs.readFileSync( - path.join( profilesPath, filename ), - 'utf8' - ) ); - const $ = cheerio.load( marked( profileMarkdown ) ); - - let hasTitleError = false; - - if ( $( 'h1' ).length !== 1 ) { - profileError( - 'Expected 1 first-level heading but found %d', - $( 'h1' ).length - ); - hasTitleError = true; - } - - if ( ! $( 'h1' ).parent().is( 'body' ) ) { - profileError( - 'The main title is wrapped inside of another element.' - ); - } - - const companyName = $( 'h1' ).text(); - - if ( ! /[a-z]/i.test( companyName ) ) { - profileError( - 'Company name looks wrong: "%s"', - companyName - ); - hasTitleError = true; - } - - const filenameBase = filename.replace( /\.md$/, '' ); - const filenameExpected = companyNameToProfileFilename( companyName ); - if ( - ! hasTitleError && - filenameBase !== filenameExpected && - // Some profile files just have shorter names than the company name, - // which is fine. - filenameExpected.substring( 0, filenameBase.length + 1 ) !== filenameBase + '-' - ) { - profileError( - 'Company title "%s" doesn\'t match filename (expected ~ "%s.md")', - companyName, - filenameExpected - ); - } - - const readmeEntry = readmeCompanies.find( - readmeEntry => readmeEntry.linkedFilename === filename - ); - - if ( filename !== 'example.md' && ! readmeEntry ) { - profileError( 'No link to company profile from readme' ); - } - - // Build and validate list of headings contained in this Markdown profile. - - const profileHeadings = []; - - $( 'h2' ).each( ( i, el ) => { - const headingName = $( el ).html(); - - if ( ! $( el ).parent().is( 'body' ) ) { - profileError( - 'The section heading for "%s" is wrapped inside of another element.', - headingName - ); - } - - if ( profileHeadings.indexOf( headingName ) >= 0 ) { - profileError( - 'Duplicate section: "%s".', - headingName - ); - } else { - // Track headings for this profile - profileHeadings.push( headingName ); - - // Track heading counts across all profiles - if ( ! allProfileHeadings[ headingName ] ) { - allProfileHeadings[ headingName ] = []; - } - allProfileHeadings[ headingName ].push( filename ); - } - - if ( headingsAll.indexOf( headingName ) === -1 ) { - profileError( - 'Invalid section: "%s". Expected one of: %s', - headingName, - JSON.stringify( headingsAll ) - ); - } - } ); - - headingsRequired.forEach( headingName => { - if ( profileHeadings.indexOf( headingName ) === -1 ) { - profileError( - 'Required section "%s" not found.', - headingName - ); - } - } ); - - // 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 ) => { - const $el = $( el ); - - if ( $el.is( 'h1' ) ) { - return; - } - - if ( $el.is( 'h2' ) ) { - 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

. - addTargetBlankAndExternalLinkIcons(el) - profileContent[ currentHeading ] = ( - profileContent[ currentHeading ] - + '\n\n' + $.html( el ) - ).trim(); - } else { - profileError( - 'Content is not part of any section: %s', - $.html( el ).replace( /\n/g, '' ) - ); - } - } ); - - Object.keys( profileContent ).forEach( heading => { - const sectionText = profileContent[ heading ] - .replace( /<[^>]+>/g, '' ) - .trim(); - if ( ! sectionText ) { - profileError( - 'Empty section: "%s". Fill it in or leave it out instead.', - heading - ); - } - } ); - - // 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 = { - readme: readmeEntry.isIncomplete, - sections: ( - profileHeadings.length === 1 && - profileHeadings[ 0 ] === 'Company blurb' - ), - content: /⚠/.test( profileContent.companyBlurb ), - }; - const incompleteCount = Object.values( isIncomplete ) - .reduce( ( sum, v ) => sum + ( v ? 1 : 0 ), 0 ); - - // incompleteCount === 0: Profile is incomplete; all 3 indicators are consistent - // incompleteCount === 3: Profile is "complete"; all 3 indicators are consistent - if ( incompleteCount === 1 ) { - if ( isIncomplete.readme ) { - profileError( - 'Profile looks complete, but the main readme contains a warning emoji.' - ); - } else if ( isIncomplete.sections ) { - profileError( - 'Profile is marked as complete, but it only contains a "Company blurb" heading.' - ) - } else { // isIncomplete.content - profileError( - 'Profile looks complete, but the "Company blurb" contains a warning emoji.' - ); - } - } else if ( incompleteCount === 2 ) { - if ( ! isIncomplete.readme ) { - profileError( - 'Profile looks incomplete, but the main readme does not contain a warning emoji.' - ); - } else if ( ! isIncomplete.sections ) { - profileError( - 'Profile is marked as incomplete, but it contains multiple sections.' - + '\nPlease remove the warning emoji from the "Company blurb" section and the main readme.' - ) - } else { // ! isIncomplete.content - profileError( - 'Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.' - ); - } - } - } - } ); - - const profileHeadingCounts = {}; - Object.keys( allProfileHeadings ).forEach( heading => { - profileHeadingCounts[ heading ] = allProfileHeadings[ heading ].length; - } ); - - if ( errors.length > 0 ) { - return { - ok: false, - errors, - profileFilenames, - profileHeadingCounts, - } - } - - return { - ok: true, - profileFilenames, - profileHeadingCounts, - companies: readmeCompanies, - readmeContent, - }; +exports.parseFromDirectory = (contentPath) => { + const companyNamesSeen = {}; + let errors = []; + + function error(filename, msg, ...params) { + errors.push({ + filename, + message: util.format(msg, ...params), + }); + } + + // Build list of Markdown files containing company profiles. + const profilesPath = path.join(contentPath, "company-profiles"); + const profileFilenames = fs.readdirSync(profilesPath); + + // Scan the company table in the readme. + + const readmeCompanies = []; + + const readmeMarkdown = stripExtraChars( + fs.readFileSync(path.join(contentPath, "README.md"), "utf8") + ); + + let inTable = false; + readmeMarkdown.split("\n").forEach((line) => { + if (/^\s*-+\s*\|\s*-+\s*\|\s*-+\s*$/.test(line)) { + inTable = true; + } else if (/^\s*$/.test(line)) { + inTable = false; + } else if (inTable) { + const fields = line.split("|"); + if (fields.length !== 3) { + readmeError( + "Expected 3 table cells but found %d: %s", + fields.length, + line + ); + } + } + }); + + const $ = cheerio.load(marked.parse(readmeMarkdown)); + + function readmeError(msg, ...params) { + error("README.md", msg, ...params); + } + + const mainUrl = "remoteintech.company"; + + function addTargetBlankAndExternalLinkIcons(el) { + if (el.type === "tag") { + const anchorTagElements = el.children.filter( + (element) => element.name === "a" + ); + if (anchorTagElements.length > 0) { + anchorTagElements.forEach((element) => { + const url = element.attribs.href; + const urlInfo = getUrlInfo(url); + + if (urlInfo.is_email || urlInfo.is_internal) { + return; + } + + element.attribs.target = "_blank"; + + $element = $(element); + $element.append( + ' ' + ); + }); + } + + if (el.children && el.children.length) { + el.children.forEach((element) => { + addTargetBlankAndExternalLinkIcons(element); + }); + } + } + } + + /** + * Getting info about the url. It includes checking isEmail of isInternal + * @param {*} url + */ + function getUrlInfo(url) { + const data = {}; + + if (url.match(/^mailto:/)) { + // checking url email or not + data.is_email = true; + return data; + } + + const mainDomainFromGivenUrl = extractMainDomainFromUrl(url); + + // checking url is email or not + if (mainDomainFromGivenUrl !== mainUrl) { + data.is_internal = false; + return data; + } else { + data.is_internal = true; + } + + return data; + } + + /** + * Extracting main domain from the url + * @param {*} 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 data = domainRe.exec(url); + + const splittedDomain = data[2].split("."); + + if (splittedDomain.length === 2) { + // check extra subdomain is present or not + return data[2]; + } + + return ( + splittedDomain[splittedDomain.length - 2] + + "." + + splittedDomain[splittedDomain.length - 1] + ); // return only main domain address + } + + 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"); + // Skip the table header row. + return; + } + const $td = $tr.children("td"); + + const websiteUrl = $td.eq(1).text(); + const websiteText = websiteUrl + .replace(/^https?:\/\//, "") + .replace(/^www\./, "") + .replace(/\/$/, ""); + + const readmeEntry = { + // 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()), + websiteUrl, + websiteText, + shortRegion: $td.eq(2).text().trim(), + }; + + 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( + "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, "") + ); + } + + if ( + lastCompanyName && + readmeEntry.name.toLowerCase() < lastCompanyName.toLowerCase() + ) { + readmeError( + 'Company is listed out of order: "%s" (should be before "%s")', + readmeEntry.name, + lastCompanyName + ); + } + lastCompanyName = readmeEntry.name; + + const $profileLink = $td.eq(0).find("a"); + + if ($profileLink.length === 1) { + const match = $profileLink + .attr("href") + .match(/^\/company-profiles\/(.*\.md)$/); + + if (match) { + readmeEntry.linkedFilename = match[1]; + if (profileFilenames.indexOf(readmeEntry.linkedFilename) === -1) { + readmeError( + 'Missing company profile for "%s", or broken link: "%s"', + readmeEntry.name, + $profileLink.attr("href") + ); + } + + const nameCheck = $profileLink.text().trim(); + if (nameCheck !== readmeEntry.name) { + readmeError( + "Extra text in company name: %s, %s", + jsonStringifyUnicodeEscaped(nameCheck), + jsonStringifyUnicodeEscaped(readmeEntry.name) + ); + } + } else { + readmeError( + 'Invalid link to company profile for "%s": "%s"', + readmeEntry.name, + $profileLink.attr("href") + ); + } + } else { + readmeError( + 'Company "%s" has no linked Markdown profile ("/company-profiles/%s.md")', + readmeEntry.name, + companyNameToProfileFilename(readmeEntry.name) + ); + } + + // 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"); + + // Rewrite company profile link to the correct URL for the static site + if ($profileLink.length) { + $profileLink.attr( + "href", + $profileLink + .attr("href") + .replace(/^\/company-profiles\//, "/") + .replace(/\.md$/, "/") + ); + } + + // Rewrite external website link (target="_blank" etc, shorter text) + const $websiteLink = $td.eq(1).children().eq(0); + $websiteLink + .attr("target", "_blank") + .attr("rel", "noopener noreferrer") + .text(websiteText); + + readmeCompanies.push(readmeEntry); + }); + + const readmeContent = $("body").html(); + + // Scan the individual Markdown files containing the company profiles. + + const allProfileHeadings = {}; + + profileFilenames.forEach((filename) => { + function profileError(msg, ...params) { + error(filename, msg, ...params); + } + + const profileMarkdown = stripExtraChars( + fs.readFileSync(path.join(profilesPath, filename), "utf8") + ); + const $ = cheerio.load(marked.parse(profileMarkdown)); + + let hasTitleError = false; + + if ($("h1").length !== 1) { + profileError( + "Expected 1 first-level heading but found %d", + $("h1").length + ); + hasTitleError = true; + } + + if (!$("h1").parent().is("body")) { + profileError("The main title is wrapped inside of another element."); + } + + const companyName = $("h1").text(); + + if (!/[a-z]/i.test(companyName)) { + profileError('Company name looks wrong: "%s"', companyName); + hasTitleError = true; + } + + const filenameBase = filename.replace(/\.md$/, ""); + const filenameExpected = companyNameToProfileFilename(companyName); + if ( + !hasTitleError && + filenameBase !== filenameExpected && + // Some profile files just have shorter names than the company name, + // which is fine. + filenameExpected.substring(0, filenameBase.length + 1) !== + filenameBase + "-" + ) { + profileError( + 'Company title "%s" doesn\'t match filename (expected ~ "%s.md")', + companyName, + filenameExpected + ); + } + + const readmeEntry = readmeCompanies.find( + (readmeEntry) => readmeEntry.linkedFilename === filename + ); + + if (filename !== "example.md" && !readmeEntry) { + profileError("No link to company profile from readme"); + } + + // Build and validate list of headings contained in this Markdown profile. + + const profileHeadings = []; + + $("h2").each((i, el) => { + const headingName = $(el).html(); + + if (!$(el).parent().is("body")) { + profileError( + 'The section heading for "%s" is wrapped inside of another element.', + headingName + ); + } + + if (profileHeadings.indexOf(headingName) >= 0) { + profileError('Duplicate section: "%s".', headingName); + } else { + // Track headings for this profile + profileHeadings.push(headingName); + + // Track heading counts across all profiles + if (!allProfileHeadings[headingName]) { + allProfileHeadings[headingName] = []; + } + allProfileHeadings[headingName].push(filename); + } + + if (headingsAll.indexOf(headingName) === -1) { + profileError( + 'Invalid section: "%s". Expected one of: %s', + headingName, + JSON.stringify(headingsAll) + ); + } + }); + + headingsRequired.forEach((headingName) => { + if (profileHeadings.indexOf(headingName) === -1) { + profileError('Required section "%s" not found.', headingName); + } + }); + + // 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) => { + const $el = $(el); + + if ($el.is("h1")) { + return; + } + + if ($el.is("h2")) { + 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

. + addTargetBlankAndExternalLinkIcons(el); + profileContent[currentHeading] = ( + profileContent[currentHeading] + + "\n\n" + + $.html(el) + ).trim(); + } else { + profileError( + "Content is not part of any section: %s", + $.html(el).replace(/\n/g, "") + ); + } + }); + + Object.keys(profileContent).forEach((heading) => { + const sectionText = profileContent[heading] + .replace(/<[^>]+>/g, "") + .trim(); + if (!sectionText) { + profileError( + 'Empty section: "%s". Fill it in or leave it out instead.', + heading + ); + } + }); + + // 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 = { + readme: readmeEntry.isIncomplete, + sections: + profileHeadings.length === 1 && + profileHeadings[0] === "Company blurb", + content: /⚠/.test(profileContent.companyBlurb), + }; + const incompleteCount = Object.values(isIncomplete).reduce( + (sum, v) => sum + (v ? 1 : 0), + 0 + ); + + // incompleteCount === 0: Profile is incomplete; all 3 indicators are consistent + // incompleteCount === 3: Profile is "complete"; all 3 indicators are consistent + if (incompleteCount === 1) { + if (isIncomplete.readme) { + profileError( + "Profile looks complete, but the main readme contains a warning emoji." + ); + } else if (isIncomplete.sections) { + profileError( + 'Profile is marked as complete, but it only contains a "Company blurb" heading.' + ); + } else { + // isIncomplete.content + profileError( + 'Profile looks complete, but the "Company blurb" contains a warning emoji.' + ); + } + } else if (incompleteCount === 2) { + if (!isIncomplete.readme) { + profileError( + "Profile looks incomplete, but the main readme does not contain a warning emoji." + ); + } else if (!isIncomplete.sections) { + profileError( + "Profile is marked as incomplete, but it contains multiple sections." + + '\nPlease remove the warning emoji from the "Company blurb" section and the main readme.' + ); + } else { + // ! isIncomplete.content + profileError( + 'Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.' + ); + } + } + } + }); + + const profileHeadingCounts = {}; + Object.keys(allProfileHeadings).forEach((heading) => { + profileHeadingCounts[heading] = allProfileHeadings[heading].length; + }); + + if (errors.length > 0) { + return { + ok: false, + errors, + profileFilenames, + profileHeadingCounts, + }; + } + + return { + ok: true, + profileFilenames, + profileHeadingCounts, + companies: readmeCompanies, + readmeContent, + }; }; /** * Build search index data from the result of parseFromDirectory(). */ -exports.buildSearchData = data => { - const textData = []; +exports.buildSearchData = (data) => { + const textData = []; - data.companies.forEach( ( company, i ) => { - const thisTextData = { - id: String( i ), - nameText: company.name, - websiteText: company.websiteText, - }; + data.companies.forEach((company, i) => { + const thisTextData = { + id: String(i), + nameText: company.name, + websiteText: company.websiteText, + }; - if ( company.shortRegion ) { - thisTextData.shortRegion = company.shortRegion; - } + 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; - } - } ); + 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 ); - } ); + textData.push(thisTextData); + }); - const index = lunr( function() { - this.field( 'nameText' ); - this.field( 'websiteText' ); - this.field( 'shortRegion' ); + const index = lunr(function () { + this.field("nameText"); + this.field("websiteText"); + this.field("shortRegion"); - Object.keys( exports.headingPropertyNames ).forEach( h => { - this.field( h ); - } ); + 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/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 ); + // 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 ] ) ); - } ); + Object.keys(textData).forEach((c) => this.add(textData[c])); + }); - const headings = getHeadingPropertyNames(); - headings.nameText = 'Company name'; - headings.websiteText = 'Website'; - headings.shortRegion = 'Region'; + const headings = getHeadingPropertyNames(); + headings.nameText = "Company name"; + headings.websiteText = "Website"; + headings.shortRegion = "Region"; - return { index, textData, headings }; + return { index, textData, headings }; }; From cce5c06faee4e153ec2f0c425339d3a03cdb4ad7 Mon Sep 17 00:00:00 2001 From: Sandy McFadden Date: Fri, 4 Feb 2022 21:24:39 -0400 Subject: [PATCH 08/25] Fix formatting changes previously made --- lib/index.js | 1202 +++++++++++++++++++++++++------------------------- 1 file changed, 604 insertions(+), 598 deletions(-) diff --git a/lib/index.js b/lib/index.js index 857694ec..8a0811c4 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,655 +1,661 @@ #!/usr/bin/env node -const fs = require("fs"); -const path = require("path"); -const util = require("util"); +const fs = require( 'fs' ); +const path = require( 'path' ); +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 */ -const headingsRequired = ["Company blurb"]; -const headingsOptional = [ - "Company size", - "Remote status", - "Region", - "Company technologies", - "Office locations", - "How to apply", +const headingsRequired = [ + 'Company blurb', ]; -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 */ -function companyNameToProfileFilename(companyName) { - return companyName - .toLowerCase() - .replace(/&/g, " and ") - .replace(/'/g, "") - .replace(/[^a-z0-9]+/gi, "-") - .replace(/^-|-$/g, ""); +function companyNameToProfileFilename( companyName ) { + return companyName.toLowerCase() + .replace( /&/g, ' and ' ) + .replace( /'/g, '' ) + .replace( /[^a-z0-9]+/gi, '-' ) + .replace( /^-|-$/g, '' ); } exports.companyNameToProfileFilename = companyNameToProfileFilename; // adapted from https://gist.github.com/RandomEtc/2657669 -function jsonStringifyUnicodeEscaped(obj) { - return JSON.stringify(obj).replace(/[\u007f-\uffff]/g, (c) => { - return "\\u" + ("0000" + c.charCodeAt(0).toString(16)).slice(-4); - }); +function jsonStringifyUnicodeEscaped( obj ) { + return JSON.stringify( obj ).replace( /[\u007f-\uffff]/g, c => { + return '\\u' + ( '0000' + c.charCodeAt( 0 ).toString( 16 ) ).slice( -4 ); + } ); } 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(""); +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, ""); +function stripExtraChars( text ) { + return text.replace( /\ufe0f/g, '' ); } exports.stripExtraChars = stripExtraChars; + /** * Other exports */ function getHeadingPropertyNames() { - return headingsAll.reduce((acc, val) => { - acc[toIdentifierCase(val)] = val; - return acc; - }, {}); + return headingsAll.reduce( ( acc, val ) => { + acc[ toIdentifierCase( val ) ] = val; + return acc; + }, {} ); } exports.headingPropertyNames = getHeadingPropertyNames(); + /** * The main exported function * * Start with a directory including a README.md and company-profiles/*.md * files, and validate and parse the content of the Markdown files. */ -exports.parseFromDirectory = (contentPath) => { - const companyNamesSeen = {}; - let errors = []; - - function error(filename, msg, ...params) { - errors.push({ - filename, - message: util.format(msg, ...params), - }); - } - - // Build list of Markdown files containing company profiles. - const profilesPath = path.join(contentPath, "company-profiles"); - const profileFilenames = fs.readdirSync(profilesPath); - - // Scan the company table in the readme. - - const readmeCompanies = []; - - const readmeMarkdown = stripExtraChars( - fs.readFileSync(path.join(contentPath, "README.md"), "utf8") - ); - - let inTable = false; - readmeMarkdown.split("\n").forEach((line) => { - if (/^\s*-+\s*\|\s*-+\s*\|\s*-+\s*$/.test(line)) { - inTable = true; - } else if (/^\s*$/.test(line)) { - inTable = false; - } else if (inTable) { - const fields = line.split("|"); - if (fields.length !== 3) { - readmeError( - "Expected 3 table cells but found %d: %s", - fields.length, - line - ); - } - } - }); - - const $ = cheerio.load(marked.parse(readmeMarkdown)); - - function readmeError(msg, ...params) { - error("README.md", msg, ...params); - } - - const mainUrl = "remoteintech.company"; - - function addTargetBlankAndExternalLinkIcons(el) { - if (el.type === "tag") { - const anchorTagElements = el.children.filter( - (element) => element.name === "a" - ); - if (anchorTagElements.length > 0) { - anchorTagElements.forEach((element) => { - const url = element.attribs.href; - const urlInfo = getUrlInfo(url); - - if (urlInfo.is_email || urlInfo.is_internal) { - return; - } - - element.attribs.target = "_blank"; - - $element = $(element); - $element.append( - ' ' - ); - }); - } - - if (el.children && el.children.length) { - el.children.forEach((element) => { - addTargetBlankAndExternalLinkIcons(element); - }); - } - } - } - - /** - * Getting info about the url. It includes checking isEmail of isInternal - * @param {*} url - */ - function getUrlInfo(url) { - const data = {}; - - if (url.match(/^mailto:/)) { - // checking url email or not - data.is_email = true; - return data; - } - - const mainDomainFromGivenUrl = extractMainDomainFromUrl(url); - - // checking url is email or not - if (mainDomainFromGivenUrl !== mainUrl) { - data.is_internal = false; - return data; - } else { - data.is_internal = true; - } - - return data; - } - - /** - * Extracting main domain from the url - * @param {*} 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 data = domainRe.exec(url); - - const splittedDomain = data[2].split("."); - - if (splittedDomain.length === 2) { - // check extra subdomain is present or not - return data[2]; - } - - return ( - splittedDomain[splittedDomain.length - 2] + - "." + - splittedDomain[splittedDomain.length - 1] - ); // return only main domain address - } - - 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"); - // Skip the table header row. - return; - } - const $td = $tr.children("td"); - - const websiteUrl = $td.eq(1).text(); - const websiteText = websiteUrl - .replace(/^https?:\/\//, "") - .replace(/^www\./, "") - .replace(/\/$/, ""); - - const readmeEntry = { - // 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()), - websiteUrl, - websiteText, - shortRegion: $td.eq(2).text().trim(), - }; - - 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( - "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, "") - ); - } - - if ( - lastCompanyName && - readmeEntry.name.toLowerCase() < lastCompanyName.toLowerCase() - ) { - readmeError( - 'Company is listed out of order: "%s" (should be before "%s")', - readmeEntry.name, - lastCompanyName - ); - } - lastCompanyName = readmeEntry.name; - - const $profileLink = $td.eq(0).find("a"); - - if ($profileLink.length === 1) { - const match = $profileLink - .attr("href") - .match(/^\/company-profiles\/(.*\.md)$/); - - if (match) { - readmeEntry.linkedFilename = match[1]; - if (profileFilenames.indexOf(readmeEntry.linkedFilename) === -1) { - readmeError( - 'Missing company profile for "%s", or broken link: "%s"', - readmeEntry.name, - $profileLink.attr("href") - ); - } - - const nameCheck = $profileLink.text().trim(); - if (nameCheck !== readmeEntry.name) { - readmeError( - "Extra text in company name: %s, %s", - jsonStringifyUnicodeEscaped(nameCheck), - jsonStringifyUnicodeEscaped(readmeEntry.name) - ); - } - } else { - readmeError( - 'Invalid link to company profile for "%s": "%s"', - readmeEntry.name, - $profileLink.attr("href") - ); - } - } else { - readmeError( - 'Company "%s" has no linked Markdown profile ("/company-profiles/%s.md")', - readmeEntry.name, - companyNameToProfileFilename(readmeEntry.name) - ); - } - - // 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"); - - // Rewrite company profile link to the correct URL for the static site - if ($profileLink.length) { - $profileLink.attr( - "href", - $profileLink - .attr("href") - .replace(/^\/company-profiles\//, "/") - .replace(/\.md$/, "/") - ); - } - - // Rewrite external website link (target="_blank" etc, shorter text) - const $websiteLink = $td.eq(1).children().eq(0); - $websiteLink - .attr("target", "_blank") - .attr("rel", "noopener noreferrer") - .text(websiteText); - - readmeCompanies.push(readmeEntry); - }); - - const readmeContent = $("body").html(); - - // Scan the individual Markdown files containing the company profiles. - - const allProfileHeadings = {}; - - profileFilenames.forEach((filename) => { - function profileError(msg, ...params) { - error(filename, msg, ...params); - } - - const profileMarkdown = stripExtraChars( - fs.readFileSync(path.join(profilesPath, filename), "utf8") - ); - const $ = cheerio.load(marked.parse(profileMarkdown)); - - let hasTitleError = false; - - if ($("h1").length !== 1) { - profileError( - "Expected 1 first-level heading but found %d", - $("h1").length - ); - hasTitleError = true; - } - - if (!$("h1").parent().is("body")) { - profileError("The main title is wrapped inside of another element."); - } - - const companyName = $("h1").text(); - - if (!/[a-z]/i.test(companyName)) { - profileError('Company name looks wrong: "%s"', companyName); - hasTitleError = true; - } - - const filenameBase = filename.replace(/\.md$/, ""); - const filenameExpected = companyNameToProfileFilename(companyName); - if ( - !hasTitleError && - filenameBase !== filenameExpected && - // Some profile files just have shorter names than the company name, - // which is fine. - filenameExpected.substring(0, filenameBase.length + 1) !== - filenameBase + "-" - ) { - profileError( - 'Company title "%s" doesn\'t match filename (expected ~ "%s.md")', - companyName, - filenameExpected - ); - } - - const readmeEntry = readmeCompanies.find( - (readmeEntry) => readmeEntry.linkedFilename === filename - ); - - if (filename !== "example.md" && !readmeEntry) { - profileError("No link to company profile from readme"); - } - - // Build and validate list of headings contained in this Markdown profile. - - const profileHeadings = []; - - $("h2").each((i, el) => { - const headingName = $(el).html(); - - if (!$(el).parent().is("body")) { - profileError( - 'The section heading for "%s" is wrapped inside of another element.', - headingName - ); - } - - if (profileHeadings.indexOf(headingName) >= 0) { - profileError('Duplicate section: "%s".', headingName); - } else { - // Track headings for this profile - profileHeadings.push(headingName); - - // Track heading counts across all profiles - if (!allProfileHeadings[headingName]) { - allProfileHeadings[headingName] = []; - } - allProfileHeadings[headingName].push(filename); - } - - if (headingsAll.indexOf(headingName) === -1) { - profileError( - 'Invalid section: "%s". Expected one of: %s', - headingName, - JSON.stringify(headingsAll) - ); - } - }); - - headingsRequired.forEach((headingName) => { - if (profileHeadings.indexOf(headingName) === -1) { - profileError('Required section "%s" not found.', headingName); - } - }); - - // 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) => { - const $el = $(el); - - if ($el.is("h1")) { - return; - } - - if ($el.is("h2")) { - 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

. - addTargetBlankAndExternalLinkIcons(el); - profileContent[currentHeading] = ( - profileContent[currentHeading] + - "\n\n" + - $.html(el) - ).trim(); - } else { - profileError( - "Content is not part of any section: %s", - $.html(el).replace(/\n/g, "") - ); - } - }); - - Object.keys(profileContent).forEach((heading) => { - const sectionText = profileContent[heading] - .replace(/<[^>]+>/g, "") - .trim(); - if (!sectionText) { - profileError( - 'Empty section: "%s". Fill it in or leave it out instead.', - heading - ); - } - }); - - // 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 = { - readme: readmeEntry.isIncomplete, - sections: - profileHeadings.length === 1 && - profileHeadings[0] === "Company blurb", - content: /⚠/.test(profileContent.companyBlurb), - }; - const incompleteCount = Object.values(isIncomplete).reduce( - (sum, v) => sum + (v ? 1 : 0), - 0 - ); - - // incompleteCount === 0: Profile is incomplete; all 3 indicators are consistent - // incompleteCount === 3: Profile is "complete"; all 3 indicators are consistent - if (incompleteCount === 1) { - if (isIncomplete.readme) { - profileError( - "Profile looks complete, but the main readme contains a warning emoji." - ); - } else if (isIncomplete.sections) { - profileError( - 'Profile is marked as complete, but it only contains a "Company blurb" heading.' - ); - } else { - // isIncomplete.content - profileError( - 'Profile looks complete, but the "Company blurb" contains a warning emoji.' - ); - } - } else if (incompleteCount === 2) { - if (!isIncomplete.readme) { - profileError( - "Profile looks incomplete, but the main readme does not contain a warning emoji." - ); - } else if (!isIncomplete.sections) { - profileError( - "Profile is marked as incomplete, but it contains multiple sections." + - '\nPlease remove the warning emoji from the "Company blurb" section and the main readme.' - ); - } else { - // ! isIncomplete.content - profileError( - 'Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.' - ); - } - } - } - }); - - const profileHeadingCounts = {}; - Object.keys(allProfileHeadings).forEach((heading) => { - profileHeadingCounts[heading] = allProfileHeadings[heading].length; - }); - - if (errors.length > 0) { - return { - ok: false, - errors, - profileFilenames, - profileHeadingCounts, - }; - } - - return { - ok: true, - profileFilenames, - profileHeadingCounts, - companies: readmeCompanies, - readmeContent, - }; +exports.parseFromDirectory = contentPath => { + const companyNamesSeen = {}; + let errors = []; + + function error( filename, msg, ...params ) { + errors.push( { + filename, + message: util.format( msg, ...params ), + } ); + } + + // Build list of Markdown files containing company profiles. + const profilesPath = path.join( contentPath, 'company-profiles' ); + const profileFilenames = fs.readdirSync( profilesPath ); + + // Scan the company table in the readme. + + const readmeCompanies = []; + + const readmeMarkdown = stripExtraChars( fs.readFileSync( + path.join( contentPath, 'README.md' ), + 'utf8' + ) ); + + let inTable = false; + readmeMarkdown.split( '\n' ).forEach( line => { + if ( /^\s*-+\s*\|\s*-+\s*\|\s*-+\s*$/.test( line ) ) { + inTable = true; + } else if ( /^\s*$/.test( line ) ) { + inTable = false; + } else if ( inTable ) { + const fields = line.split( '|' ); + if ( fields.length !== 3 ) { + readmeError( + 'Expected 3 table cells but found %d: %s', + fields.length, + line + ); + } + } + } ); + + const $ = cheerio.load( marked.parse( readmeMarkdown ) ); + + function readmeError( msg, ...params ) { + error( 'README.md', msg, ...params ); + } + + const mainUrl = 'remoteintech.company' + + function addTargetBlankAndExternalLinkIcons (el) { + if (el.type === 'tag') { + const anchorTagElements = el.children.filter(element => element.name === 'a') + if (anchorTagElements.length > 0) { + anchorTagElements.forEach(element => { + const url = element.attribs.href + const urlInfo = getUrlInfo(url) + + if (urlInfo.is_email || urlInfo.is_internal) { + return + } + + element.attribs.target = '_blank' + + $element = $( element ) + $element.append(' ') + }) + } + + if (el.children && el.children.length) { + el.children.forEach(element => { + addTargetBlankAndExternalLinkIcons(element) + }) + } + } + } + + /** + * Getting info about the url. It includes checking isEmail of isInternal + * @param {*} url + */ + function getUrlInfo (url) { + const data = {} + + if (url.match(/^mailto:/)) { // checking url email or not + data.is_email = true + return data + } + + const mainDomainFromGivenUrl = extractMainDomainFromUrl(url) + + // checking url is email or not + if (mainDomainFromGivenUrl !== mainUrl) { + data.is_internal = false + return data + } else { + data.is_internal = true + } + + return data + } + + /** + * Extracting main domain from the url + * @param {*} 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 data = domainRe.exec(url) + + const splittedDomain = data[2].split('.') + + if (splittedDomain.length === 2) { // check extra subdomain is present or not + return data[2] + } + + return splittedDomain[splittedDomain.length - 2] + '.' + splittedDomain[splittedDomain.length - 1] // return only main domain address + } + + 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' ); + // Skip the table header row. + return; + } + const $td = $tr.children( 'td' ); + + const websiteUrl = $td.eq( 1 ).text(); + const websiteText = websiteUrl + .replace( /^https?:\/\//, '' ) + .replace( /^www\./, '' ) + .replace( /\/$/, '' ); + + const readmeEntry = { + // 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() ), + websiteUrl, + websiteText, + shortRegion: $td.eq( 2 ).text().trim(), + }; + + 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( + '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, '' ) + ); + } + + if ( + lastCompanyName && + readmeEntry.name.toLowerCase() < lastCompanyName.toLowerCase() + ) { + readmeError( + 'Company is listed out of order: "%s" (should be before "%s")', + readmeEntry.name, + lastCompanyName + ); + } + lastCompanyName = readmeEntry.name; + + const $profileLink = $td.eq( 0 ).find( 'a' ); + + if ( $profileLink.length === 1 ) { + const match = $profileLink.attr( 'href' ).match( /^\/company-profiles\/(.*\.md)$/ ); + + if ( match ) { + readmeEntry.linkedFilename = match[ 1 ]; + if ( profileFilenames.indexOf( readmeEntry.linkedFilename ) === -1 ) { + readmeError( + 'Missing company profile for "%s", or broken link: "%s"', + readmeEntry.name, + $profileLink.attr( 'href' ) + ); + } + + const nameCheck = $profileLink.text().trim(); + if ( nameCheck !== readmeEntry.name ) { + readmeError( + 'Extra text in company name: %s, %s', + jsonStringifyUnicodeEscaped( nameCheck ), + jsonStringifyUnicodeEscaped( readmeEntry.name ) + ); + } + } else { + readmeError( + 'Invalid link to company profile for "%s": "%s"', + readmeEntry.name, + $profileLink.attr( 'href' ) + ); + } + } else { + readmeError( + 'Company "%s" has no linked Markdown profile ("/company-profiles/%s.md")', + readmeEntry.name, + companyNameToProfileFilename( readmeEntry.name ) + ); + } + + // 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' ); + + // Rewrite company profile link to the correct URL for the static site + if ( $profileLink.length ) { + $profileLink.attr( + 'href', + $profileLink.attr( 'href' ) + .replace( /^\/company-profiles\//, '/' ) + .replace( /\.md$/, '/' ) + ); + } + + // Rewrite external website link (target="_blank" etc, shorter text) + const $websiteLink = $td.eq( 1 ).children().eq( 0 ); + $websiteLink + .attr( 'target', '_blank' ) + .attr( 'rel', 'noopener noreferrer' ) + .text( websiteText ); + + readmeCompanies.push( readmeEntry ); + } ); + + const readmeContent = $( 'body' ).html(); + + // Scan the individual Markdown files containing the company profiles. + + const allProfileHeadings = {}; + + profileFilenames.forEach( filename => { + function profileError( msg, ...params ) { + error( filename, msg, ...params ); + } + + const profileMarkdown = stripExtraChars( fs.readFileSync( + path.join( profilesPath, filename ), + 'utf8' + ) ); + const $ = cheerio.load( marked.parse( profileMarkdown ) ); + + let hasTitleError = false; + + if ( $( 'h1' ).length !== 1 ) { + profileError( + 'Expected 1 first-level heading but found %d', + $( 'h1' ).length + ); + hasTitleError = true; + } + + if ( ! $( 'h1' ).parent().is( 'body' ) ) { + profileError( + 'The main title is wrapped inside of another element.' + ); + } + + const companyName = $( 'h1' ).text(); + + if ( ! /[a-z]/i.test( companyName ) ) { + profileError( + 'Company name looks wrong: "%s"', + companyName + ); + hasTitleError = true; + } + + const filenameBase = filename.replace( /\.md$/, '' ); + const filenameExpected = companyNameToProfileFilename( companyName ); + if ( + ! hasTitleError && + filenameBase !== filenameExpected && + // Some profile files just have shorter names than the company name, + // which is fine. + filenameExpected.substring( 0, filenameBase.length + 1 ) !== filenameBase + '-' + ) { + profileError( + 'Company title "%s" doesn\'t match filename (expected ~ "%s.md")', + companyName, + filenameExpected + ); + } + + const readmeEntry = readmeCompanies.find( + readmeEntry => readmeEntry.linkedFilename === filename + ); + + if ( filename !== 'example.md' && ! readmeEntry ) { + profileError( 'No link to company profile from readme' ); + } + + // Build and validate list of headings contained in this Markdown profile. + + const profileHeadings = []; + + $( 'h2' ).each( ( i, el ) => { + const headingName = $( el ).html(); + + if ( ! $( el ).parent().is( 'body' ) ) { + profileError( + 'The section heading for "%s" is wrapped inside of another element.', + headingName + ); + } + + if ( profileHeadings.indexOf( headingName ) >= 0 ) { + profileError( + 'Duplicate section: "%s".', + headingName + ); + } else { + // Track headings for this profile + profileHeadings.push( headingName ); + + // Track heading counts across all profiles + if ( ! allProfileHeadings[ headingName ] ) { + allProfileHeadings[ headingName ] = []; + } + allProfileHeadings[ headingName ].push( filename ); + } + + if ( headingsAll.indexOf( headingName ) === -1 ) { + profileError( + 'Invalid section: "%s". Expected one of: %s', + headingName, + JSON.stringify( headingsAll ) + ); + } + } ); + + headingsRequired.forEach( headingName => { + if ( profileHeadings.indexOf( headingName ) === -1 ) { + profileError( + 'Required section "%s" not found.', + headingName + ); + } + } ); + + // 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 ) => { + const $el = $( el ); + + if ( $el.is( 'h1' ) ) { + return; + } + + if ( $el.is( 'h2' ) ) { + 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

. + addTargetBlankAndExternalLinkIcons(el) + profileContent[ currentHeading ] = ( + profileContent[ currentHeading ] + + '\n\n' + $.html( el ) + ).trim(); + } else { + profileError( + 'Content is not part of any section: %s', + $.html( el ).replace( /\n/g, '' ) + ); + } + } ); + + Object.keys( profileContent ).forEach( heading => { + const sectionText = profileContent[ heading ] + .replace( /<[^>]+>/g, '' ) + .trim(); + if ( ! sectionText ) { + profileError( + 'Empty section: "%s". Fill it in or leave it out instead.', + heading + ); + } + } ); + + // 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 = { + readme: readmeEntry.isIncomplete, + sections: ( + profileHeadings.length === 1 && + profileHeadings[ 0 ] === 'Company blurb' + ), + content: /⚠/.test( profileContent.companyBlurb ), + }; + const incompleteCount = Object.values( isIncomplete ) + .reduce( ( sum, v ) => sum + ( v ? 1 : 0 ), 0 ); + + // incompleteCount === 0: Profile is incomplete; all 3 indicators are consistent + // incompleteCount === 3: Profile is "complete"; all 3 indicators are consistent + if ( incompleteCount === 1 ) { + if ( isIncomplete.readme ) { + profileError( + 'Profile looks complete, but the main readme contains a warning emoji.' + ); + } else if ( isIncomplete.sections ) { + profileError( + 'Profile is marked as complete, but it only contains a "Company blurb" heading.' + ) + } else { // isIncomplete.content + profileError( + 'Profile looks complete, but the "Company blurb" contains a warning emoji.' + ); + } + } else if ( incompleteCount === 2 ) { + if ( ! isIncomplete.readme ) { + profileError( + 'Profile looks incomplete, but the main readme does not contain a warning emoji.' + ); + } else if ( ! isIncomplete.sections ) { + profileError( + 'Profile is marked as incomplete, but it contains multiple sections.' + + '\nPlease remove the warning emoji from the "Company blurb" section and the main readme.' + ) + } else { // ! isIncomplete.content + profileError( + 'Profile looks incomplete, but the "Company blurb" does not contain a warning emoji.' + ); + } + } + } + } ); + + const profileHeadingCounts = {}; + Object.keys( allProfileHeadings ).forEach( heading => { + profileHeadingCounts[ heading ] = allProfileHeadings[ heading ].length; + } ); + + if ( errors.length > 0 ) { + return { + ok: false, + errors, + profileFilenames, + profileHeadingCounts, + } + } + + return { + ok: true, + profileFilenames, + profileHeadingCounts, + companies: readmeCompanies, + readmeContent, + }; }; /** * Build search index data from the result of parseFromDirectory(). */ -exports.buildSearchData = (data) => { - const textData = []; +exports.buildSearchData = data => { + const textData = []; - data.companies.forEach((company, i) => { - const thisTextData = { - id: String(i), - nameText: company.name, - websiteText: company.websiteText, - }; + data.companies.forEach( ( company, i ) => { + const thisTextData = { + id: String( i ), + nameText: company.name, + websiteText: company.websiteText, + }; - if (company.shortRegion) { - thisTextData.shortRegion = company.shortRegion; - } + 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; - } - }); + 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); - }); + textData.push( thisTextData ); + } ); - const index = lunr(function () { - this.field("nameText"); - this.field("websiteText"); - this.field("shortRegion"); + const index = lunr( function() { + this.field( 'nameText' ); + this.field( 'websiteText' ); + this.field( 'shortRegion' ); - Object.keys(exports.headingPropertyNames).forEach((h) => { - this.field(h); - }); + 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/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); + // 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])); - }); + Object.keys( textData ).forEach( c => this.add( textData[ c ] ) ); + } ); - const headings = getHeadingPropertyNames(); - headings.nameText = "Company name"; - headings.websiteText = "Website"; - headings.shortRegion = "Region"; + const headings = getHeadingPropertyNames(); + headings.nameText = 'Company name'; + headings.websiteText = 'Website'; + headings.shortRegion = 'Region'; - return { index, textData, headings }; + return { index, textData, headings }; }; From c2874579b7ac61f8dd175e0f215e56908bbd5986 Mon Sep 17 00:00:00 2001 From: Doug Aitken Date: Sat, 5 Feb 2022 19:15:03 +0000 Subject: [PATCH 09/25] Remove space in company name Stream Native > StreamNative --- company-profiles/streamnative.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/company-profiles/streamnative.md b/company-profiles/streamnative.md index 48b807e1..118b0dc9 100644 --- a/company-profiles/streamnative.md +++ b/company-profiles/streamnative.md @@ -1,5 +1,5 @@ -# Stream Native +# StreamNative ## Company blurb From f767ee16f93856b748f436891acb03bcbdded16f Mon Sep 17 00:00:00 2001 From: Rustem Saiargaliev Date: Mon, 14 Feb 2022 09:48:16 +0100 Subject: [PATCH 10/25] add new company - voiio --- README.md | 1 + company-profiles/voiio.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 company-profiles/voiio.md diff --git a/README.md b/README.md index 858fa4e2..ab9a659d 100644 --- a/README.md +++ b/README.md @@ -601,6 +601,7 @@ Name | Website | Region [Vercel](/company-profiles/vercel.md) | https://vercel.com/ | Worldwide [Veryfi](/company-profiles/veryfi.md) | https://veryfi.com/about | Worldwide [Viperdev](/company-profiles/viperdev.md) | https://viperdev.io | Worldwide +[voiio](/company-profiles/voiio.md) | https://voiio.io | Europe [Vox Media (Product Team)](/company-profiles/vox-media.md) | https://www.voxmedia.com/ | USA, UK [Voxy](/company-profiles/voxy.md)️️ | https://boards.greenhouse.io/voxy | Brazil, USA [VSHN](/company-profiles/vshn.md)️️ | https://vshn.ch/jobs | Switzerland diff --git a/company-profiles/voiio.md b/company-profiles/voiio.md new file mode 100644 index 00000000..91359fe7 --- /dev/null +++ b/company-profiles/voiio.md @@ -0,0 +1,35 @@ +# voiio + +## Company blurb + +voiio is closing the gap between having to manage your job and private life. We are a German based B2B employee service provider, always creating new services that have a meaningful impact on people's every day lives. + +## Company size + +50-100 + +## Remote status + +Fully remote within EU is possible (as we'd like to meet each other occasionally). +We do have offices in Berlin and Hamburg that everyone is welcome to use. +Communication is happening via Discord, Email and Zoom. + +## Region + +Europe + +## Company technologies + +Python, JavaScript, Heroku. +Good chefs keep their knifes sharp. We believe the same is true for software engineers, which is why we always aim to use the best tools available and don’t mind investing into our setup. + +You can find our full tech stack on [stackshare.io/voiio](https://stackshare.io/voiio/). + +## Office locations + +Berlin, Hamburg + +## How to apply + +- [Careers page](https://voiio.de/karriere/) +- [Philosophy and hiring process](https://code.voiio.de/) From e0a376270c04f645d293fa916e0440aeb5bb100c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 12:08:44 +0000 Subject: [PATCH 11/25] Bump follow-redirects from 1.14.7 to 1.14.8 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77375ab6..5a6d895b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -581,9 +581,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "dev": true, "funding": [ { @@ -2157,9 +2157,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "dev": true }, "fs.realpath": { From 12bbb735c426c533d7c90143b9565452595999f7 Mon Sep 17 00:00:00 2001 From: Anthony Drendel Date: Mon, 21 Feb 2022 15:40:47 +0100 Subject: [PATCH 12/25] Add Shareup --- README.md | 1 + company-profiles/shareup.md | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 company-profiles/shareup.md diff --git a/README.md b/README.md index 858fa4e2..22f70236 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,7 @@ Name | Website | Region [Server Density](/company-profiles/server-density.md) | https://www.serverdensity.com | Europe [ServMask](/company-profiles/servmask.md) | https://servmask.com | Worldwide [Session](/company-profiles/session.md) | https://getsession.com | Worldwide +[Shareup](/company-profiles/shareup.md) | https://shareup.app | Europe [Shogun](/company-profiles/shogun.md) | https://getshogun.com | Worldwide [Shopify](/company-profiles/shopify.md) | https://www.shopify.com | Worldwide [SignEasy](/company-profiles/signeasy.md) | https://getsigneasy.com | Worldwide diff --git a/company-profiles/shareup.md b/company-profiles/shareup.md new file mode 100644 index 00000000..c5ec0e41 --- /dev/null +++ b/company-profiles/shareup.md @@ -0,0 +1,37 @@ +# Shareup + +## Company blurb + +Shareup is the easiest, fastest way to securely share anything with anyone. We help teams collect, organize, and make sense of the myriad of files, links, and services they use to get their work done everyday. Sharing is currently too difficult and stressful! We are obsessed with relieving that stress and anxiety. + +We are a design-led company. We define design as “how it works.” This includes everyone having an overview of the tech fundamentals, understanding the full experience flow for the customer, and contributing to the beautiful details of everything we make—from UIs to code to icons to all the assets and interfaces our customers see and touch. + +We are, at our core, a group of people passionate about building great products for our customers. We think those products require a strong design vision, solid engineering focused on performance and stability, and reacting to customer feedback. More than that, we believe every employee at Shareup has a part to play in making sure we always adhere to those principles. + +## Company size + +4 + +## Remote status + +Shareup is a remote-only company with flexible working hours and generous time off. We understand everyone’s life is different, and people should be allowed to work at the times that best suit them. Additionally, we think people do their best work when they are well-rested and rejuvenated. Every employee of Shareup receives 30 days of paid vacation per year, regardless of where they are based. + +Given our commitment to remote work and flexible working hours, we think it’s important to cultivate a strong culture of writing. We believe this improves the quality of our ideas and work, ensures everyone has equal access to information, and gives newcomers the ability to understand the evolution of Shareup as a company and product. As an example of our culture of writing, we don’t have standup meetings. Instead, everyone writes down daily logs of the goals they have, the work they’ve done, the challenges they’ve experienced, and the solutions they discovered. + +## Region + +European timezones + +## Company technologies + +Elixir, JavaScript, PostgreSQL, React, Swift, TypeScript + +For remote communication, we prefer asynchronous tools like Craft. For video, we use Zoom. For chat, which we try to use rarely, we currently use Telegram. + +## Office locations + +No offices, but most employees are based in Germany + +## How to apply + +Shareup openings are listed on the [jobs](https://shareup.app/jobs/) page. From 8c6ab5d002d559a4fb2c095c0e67c61ff4aceb4c Mon Sep 17 00:00:00 2001 From: heapwolf Date: Sun, 20 Feb 2022 12:34:15 +0100 Subject: [PATCH 13/25] add Socket Supply Co. --- README.md | 1 + company-profiles/socket-supply-co.md | 29 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 company-profiles/socket-supply-co.md diff --git a/README.md b/README.md index 858fa4e2..7ca8c226 100644 --- a/README.md +++ b/README.md @@ -523,6 +523,7 @@ Name | Website | Region [SmartCash](/company-profiles/smartcash.md) | https://www.smartcash.cc/ | Worldwide [Smile.io](/company-profiles/smile.md) | https://smile.io | Worldwide [SmugMug](/company-profiles/smugmug.md) | https://www.smugmug.com/ | Worldwide +[Socket Supply Co.](/company-profiles/socket-supply-co.md) | https://socketsupply.co | Worldwide [SoftwareMill](/company-profiles/softwaremill.md) | https://softwaremill.com/ | Europe [Soostone](/company-profiles/soostone.md) | https://www.soostone.com/ | USA [Soshace](/company-profiles/soshace.md) | https://www.soshace.com/ | Worldwide diff --git a/company-profiles/socket-supply-co.md b/company-profiles/socket-supply-co.md new file mode 100644 index 00000000..da84bf4c --- /dev/null +++ b/company-profiles/socket-supply-co.md @@ -0,0 +1,29 @@ +# Socket Supply Co + +## Company blurb + +Socket Supply Co. is how web developers use cloud computing services like AWS. Our lean, fast, local-first software helps developers be more productive, collaborate in real-time and deploy the web. + +## Company size + +6 + +## Remote status + +Socket Supply Co. is a mostly remote company with offices in Berlin and New York. + +## Region + +Worldwide + +## Company technologies + +JavaScript, HTML, CSS, C++, Objective-C++ Cocoa, GTK, Win32, Rust, Git, Discord, IRC + +## Office locations + +Berlin, New York + +## How to apply + +Socket Supply co. openings are listed on the [jobs](https://socketsupply.co/jobs/) page. From aa39431774f4aa8f00a2841e5393fcd4ff03df86 Mon Sep 17 00:00:00 2001 From: Basim Hennawi Date: Thu, 24 Feb 2022 16:30:25 +0100 Subject: [PATCH 14/25] add "appinio" company asa record in readme.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 47c91c56..f668f8d9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Name | Website | Region [Animalz](/company-profiles/animalz.md) | https://www.animalz.co | USA [Anomali](/company-profiles/anomali.md) | https://www.anomali.com/company/careers | USA, UK, Singapore [apartment therapy](/company-profiles/apartment-therapy.md) | http://www.apartmenttherapy.com/ | USA +[Appinio](/company-profiles/appinio.md) | https://appinio.com/ | Germany [Appstractor Corporation](/company-profiles/appstractor.md) | https://www.appstractor.com/ | USA, UK, Israel [Appwrite](/company-profiles/appwrite.md) | https://appwrite.io | Worldwide [argyle](/company-profiles/argyle.md) | https://argyle.com/ | Worldwide From b7ab0e4a2bb5d5bbebf0f3a5b760bd65b174eaae Mon Sep 17 00:00:00 2001 From: Basim Hennawi Date: Thu, 24 Feb 2022 16:44:05 +0100 Subject: [PATCH 15/25] Create appinio.md --- company-profiles/appinio.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 company-profiles/appinio.md diff --git a/company-profiles/appinio.md b/company-profiles/appinio.md new file mode 100644 index 00000000..103f2ee0 --- /dev/null +++ b/company-profiles/appinio.md @@ -0,0 +1,30 @@ +# Appinio + + +## Company blurb + +Appinio enables anyone to survey highly specific target groups. Conduct market research in hours rather than weeks and make better decisions using customer opinions. + +## Company size + +100 - 200 + +## Remote status + +Need to be registered in Europe. Work remote anywhere in Europe (EU) or anywhere in the world for maximum 6 consecutive months (depends on each country's rules). + +## Region + +Europe + +## Company technologies + +Angular / Flutter / Node.js / MongoDB / AWS + +## Office locations + +Hamburg, Germany + +## How to apply + +[Apply Here](https://www.appinio.com/en/careers)! From 4fbf7a2badcfbfcc62df1127cf3eca7901b19889 Mon Sep 17 00:00:00 2001 From: Basim Hennawi Date: Wed, 2 Mar 2022 16:11:07 +0100 Subject: [PATCH 16/25] update appinio's region --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f668f8d9..f57b97f3 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Name | Website | Region [Animalz](/company-profiles/animalz.md) | https://www.animalz.co | USA [Anomali](/company-profiles/anomali.md) | https://www.anomali.com/company/careers | USA, UK, Singapore [apartment therapy](/company-profiles/apartment-therapy.md) | http://www.apartmenttherapy.com/ | USA -[Appinio](/company-profiles/appinio.md) | https://appinio.com/ | Germany +[Appinio](/company-profiles/appinio.md) | https://appinio.com/ | Europe [Appstractor Corporation](/company-profiles/appstractor.md) | https://www.appstractor.com/ | USA, UK, Israel [Appwrite](/company-profiles/appwrite.md) | https://appwrite.io | Worldwide [argyle](/company-profiles/argyle.md) | https://argyle.com/ | Worldwide From d4385d5409b3478a6b5aca36b9ee3d27bb5a3bd1 Mon Sep 17 00:00:00 2001 From: Yaroslav Markin Date: Wed, 9 Mar 2022 13:33:20 +0300 Subject: [PATCH 17/25] Update evil-martians.md --- company-profiles/evil-martians.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/company-profiles/evil-martians.md b/company-profiles/evil-martians.md index 93928678..e8668c95 100644 --- a/company-profiles/evil-martians.md +++ b/company-profiles/evil-martians.md @@ -20,29 +20,23 @@ Remote-first distrbuted team with several offices for offline meetups. ## Region -United States (New York, San Francisco), Russia (Moscow), Japan (Osaka) +United States (NY), Portugal, Japan. ## Company technologies -Ruby, Ruby on Rails, Ruby, Go, Elixir, Node.js, Rust, JVM languages. -JavaScript/TypeScript, React/Redux, Vue, Webpack, PostCSS, GraphQL. -Swift, React Native. -Kubernetes, Chef, PostgreSQL, Redis, Elasticsearch. +Ruby and Rails, Go, TypeScript, Elixir, Rust. TypeScript, React/Redux, Svelte, Vue, PostCSS, GraphQL. iOS with Swift. Blockchain (Stellar). Machine Learning: PyTorch, Catalyst. Kubernetes, Docker. ## Office locations 195 Montague St. Brooklyn, NY 11201 -156 2nd St. -San Francisco, CA 94105 - -Botanichesky Lane, 5 -Moscow, 129090, Russia +Vila Nova de Gaia, Rua Alexandre Oneill, 38 +Porto, Portugal 4400-008 3‑6‑1 Kitakyuhojimachi, Chuo‑ku Osaka, 541-0057, Japan ## How to apply -Write to apply at [obey@evilmartians.com](mailto:obey@evilmartians.com) or make an open source task via [cultofmartians.com](https://cultofmartians.com/). +First, please check if there are openings at the [site](https://evilmartians.com/) or [our socials](https://twitter.com/evilmartians). Write to apply at [obey@evilmartians.com](mailto:obey@evilmartians.com); make sure to write a cover letter and include links to you open source projects or contributions, no matter how small. From 3b60f00212edd73c03549f02605f7be4a025cd2c Mon Sep 17 00:00:00 2001 From: Michele Orselli Date: Thu, 10 Mar 2022 11:16:23 +0100 Subject: [PATCH 18/25] adds Spreaker --- company-profiles/spreaker.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 company-profiles/spreaker.md diff --git a/company-profiles/spreaker.md b/company-profiles/spreaker.md new file mode 100644 index 00000000..6c15c6e8 --- /dev/null +++ b/company-profiles/spreaker.md @@ -0,0 +1,36 @@ +# Spreaker + +## Company blurb + +Since 2010 [Spreaker](https://www.spreaker.com/) has been striving for technical and customer service excellence. Within the last years we have moved from being a podcast hosting and distribution platform to expanding and innovating how content producers monetize and hone their craft. + +Our global podcast hosting network delivers more than 300 million listens per month across 4 continents, enabling more than 50 thousand podcast producers to get heard and create successful podcasts. We ensure to bridge the gap between creativity and passion to profitable businesses. + +Since late 2020 Spreaker is part of [iHeartMedia](https://www.iheartmedia.com/). + +## Company size + +~50 + +## Remote status + +We are a full remote company since 2016. You can choose to work wherever you like, and there is an allocated personal budget for home equipment or coworking fee + + +## Region + +Tech team is all located in Europe (more or less between CET-2 and CET +2) + +## Company technologies + +In order to provide our service we mantain several application, using a wide range of technologies: +- Kubernetes, Lambda, SQS, Postgres, Redis, Prometheus +- PHP, Javascript, Typescript, Scala, Android, Swift, Kotlin + +## Office locations + +The internet. + +## How to apply + +Contact Us: https://careers.spreaker.com/opportunities/ From 39b204c25b67e869b8b7d7827eba4fdd6d794120 Mon Sep 17 00:00:00 2001 From: Michele Orselli Date: Thu, 10 Mar 2022 12:18:19 +0100 Subject: [PATCH 19/25] tweaks description --- README.md | 1 + company-profiles/spreaker.md | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f57b97f3..f7515153 100644 --- a/README.md +++ b/README.md @@ -530,6 +530,7 @@ Name | Website | Region [Soostone](/company-profiles/soostone.md) | https://www.soostone.com/ | USA [Soshace](/company-profiles/soshace.md) | https://www.soshace.com/ | Worldwide [Spoqa](/company-profiles/spoqa.md) | https://www.spoqa.com/ | Republic of Korea, Japan +[Spreaker](/company-profiles/spreaker.md) | https://spreaker.com/ | US - Europe [Spreedly](/company-profiles/spreedly.md) | https://spreedly.com/ | USA [Spruce](/company-profiles/spruce.md) | https://getspruce.com/ | North America, Latin America [Stack Exchange](/company-profiles/stack-exchange.md) | https://stackexchange.com/ | Worldwide diff --git a/company-profiles/spreaker.md b/company-profiles/spreaker.md index 6c15c6e8..cf2d535d 100644 --- a/company-profiles/spreaker.md +++ b/company-profiles/spreaker.md @@ -14,16 +14,25 @@ Since late 2020 Spreaker is part of [iHeartMedia](https://www.iheartmedia.com/). ## Remote status -We are a full remote company since 2016. You can choose to work wherever you like, and there is an allocated personal budget for home equipment or coworking fee +We are a full remote company since 2016. You can choose to work wherever you like, being your home, a cafeteria, a coworking, you name it. +The company provides an annual personal budget for buying home equipment or for paying a coworking. + +Connecting and empathising with colleagues in a remote environment is challenging. During the years we learned to create moments in the day-by-day work to ease this. Just to give you an idea we have: +- Donuts: opt-in, random, 1-to-1 chat about all topics but work +- Monthly Trivia: where teams challenge each other like in a TV show quiz + +Several people are located in Italy and sometimes people who live nearby meet and work together for a day. + +Once a year we gather together for a company retreat, usually in Europe. ## Region -Tech team is all located in Europe (more or less between CET-2 and CET +2) +We are both in US and Europe. Tech team is all located in Europe (more or less between CET-2 and CET +2) ## Company technologies -In order to provide our service we mantain several application, using a wide range of technologies: +In order to provide our service we mantain several application, using a wide range of technologies, just to name a few: - Kubernetes, Lambda, SQS, Postgres, Redis, Prometheus - PHP, Javascript, Typescript, Scala, Android, Swift, Kotlin @@ -33,4 +42,4 @@ The internet. ## How to apply -Contact Us: https://careers.spreaker.com/opportunities/ +You can find all open position here: https://careers.spreaker.com/opportunities/ From 361dfa2f64418476cf72ce0bc490350ce6e6ae62 Mon Sep 17 00:00:00 2001 From: Michele Orselli Date: Thu, 10 Mar 2022 12:20:02 +0100 Subject: [PATCH 20/25] fixes typo --- company-profiles/spreaker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/company-profiles/spreaker.md b/company-profiles/spreaker.md index cf2d535d..bb441c3f 100644 --- a/company-profiles/spreaker.md +++ b/company-profiles/spreaker.md @@ -42,4 +42,4 @@ The internet. ## How to apply -You can find all open position here: https://careers.spreaker.com/opportunities/ +You can find all open positions here: https://careers.spreaker.com/opportunities/ From 4a7dc6ce3b63d2a64c59345fda6e799c8973c410 Mon Sep 17 00:00:00 2001 From: Tobias Speicher Date: Tue, 22 Mar 2022 09:42:18 +0100 Subject: [PATCH 21/25] refactor: replace deprecated String.prototype.substr() .substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated Signed-off-by: Tobias Speicher --- lib/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 8a0811c4..4faee7a6 100755 --- a/lib/index.js +++ b/lib/index.js @@ -59,8 +59,8 @@ function toIdentifierCase( text ) { return word.toLowerCase(); } return ( - word.substr( 0, 1 ).toUpperCase() - + word.substr( 1 ).toLowerCase() + word.slice( 0, 1 ).toUpperCase() + + word.slice( 1 ).toLowerCase() ); } ) .join( '' ); From 26f7eac3049b08543ebfb9a98bdff2fd958d5426 Mon Sep 17 00:00:00 2001 From: omesser Date: Thu, 24 Mar 2022 19:03:56 +0200 Subject: [PATCH 22/25] M --- README.md | 1 + company-profiles/iterative.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 company-profiles/iterative.md diff --git a/README.md b/README.md index f7515153..3e0a0c75 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,7 @@ Name | Website | Region [IPInfo](/company-profiles/ipinfo.md) | https://ipinfo.io/ | Worldwide [IPS Group, Inc.](/company-profiles/ips-group-inc.md) | https://www.ipsgroupinc.com/ | USA [iRonin](/company-profiles/ironin.md) | https://www.ironin.it/ | Worldwide +[Iterative](/company-profiles/iterative.md) | https://www.iterative.ai/ | Worldwide [iwantmyname](/company-profiles/iwantmyname.md) | https://iwantmyname.com/ | Worldwide [Jackson River](/company-profiles/jackson-river.md) | https://jacksonriver.com/ | USA [Jaya Tech](/company-profiles/jaya-tech.md) | https://jaya.tech/ | Worldwide diff --git a/company-profiles/iterative.md b/company-profiles/iterative.md new file mode 100644 index 00000000..944a2132 --- /dev/null +++ b/company-profiles/iterative.md @@ -0,0 +1,33 @@ +# Iterative + +## Company blurb + +AI teams face challenges that require new technologies. We build these technologies. Existing data warehouses and data lakes do not fit unstructured datasets like text, images, and videos. + +See: [about](http://iterative.ai/about) + +## Company size + +45-100 + +## Remote status + +100% remote: + +> Iterative is a 100% remote team. We currently span 19 countries and 18 time zones + +## Region + +Anywhere + +## Company technologies + +Python, Golang, JavaScript, Typescript, React, Terraform, AWS, GCP, Azure + +## Office locations + +The team is 100% remote + +## How to apply + +Job openings can be found [here](https://jobs.lever.co/iterative?lever-origin=applied&lever-source%5B%5D=gh.remoteintech) From 01eb7500012b75903ddbf7bd940c482592b76837 Mon Sep 17 00:00:00 2001 From: omesser Date: Thu, 24 Mar 2022 19:14:32 +0200 Subject: [PATCH 23/25] More text --- company-profiles/iterative.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/company-profiles/iterative.md b/company-profiles/iterative.md index 944a2132..c2dfc7aa 100644 --- a/company-profiles/iterative.md +++ b/company-profiles/iterative.md @@ -4,7 +4,12 @@ AI teams face challenges that require new technologies. We build these technologies. Existing data warehouses and data lakes do not fit unstructured datasets like text, images, and videos. -See: [about](http://iterative.ai/about) +✅ Check out the [Website](https://dvc.org/) and [Docs](http://dvc.org/doc) + +✅ Check out our open-source projects in [GitHub](http://github.com/iterative) + +✅ Finally, take a look at our [Blog](http://dvc.org/blog) and [YouTube](https://www.youtube.com/channel/UC37rp97Go-xIX3aNFVHhXfQ) + ## Company size @@ -16,6 +21,33 @@ See: [about](http://iterative.ai/about) > Iterative is a 100% remote team. We currently span 19 countries and 18 time zones +We value great collaboration and communication skills, both among internal teams and in how we interact with our users. We take care to balance and be responsive to the needs of our open source community as well as our enterprise customers. + +### WHAT WE OFFER + +🌎 Team is distributed remotely worldwide. + +🤗 ****Open source-first company - your work will be visible and will be used by thousands of developers every day! This feels great! Check out our [Discord](http://dvc.org/chat) and [GitHub](http://github.com/iterative). + +⚓️ Engineering team is involved in product discussions and planning. We do it openly via GitHub or Discord chat. Well-defined process that we all participate in improving. + +🎤 Besides building the products and the business we participate in conferences (PyCon, PyData, O'Reilly AI, etc). We encourage and support the team in giving talks, writing blog-posts, and other activities. + +⚕️ Great health coverage (medical, dental, vision) for you and your family, 100% paid by us (US only, but can discuss and reimburse, adjust the salary in other locations) + +🛡️ 401K with 100% match up to 4% of annual salary (US only, but can discuss and reimburse, adjust the salary in other locations) + +### WE TAKE CARE OF OUR PEOPLE + +As a distributed company, diversity drives our identity. Whether you’re looking to launch a new career or grow an existing one, *ITERATIVE* is the type of company where you can balance great work with great life. Your age is only a number. It doesn’t matter if you’re just out of college or your children are; we need you for what you can do. + +We strive to have parity of benefits across regions and while regulations differ from place to place, we believe taking care of our people is the right thing to do. + +- Competitive pay based on the work you do here and not your previous salary +- Great health coverage for you and your family, 100% paid by us (US only, but can discuss and reimburse, adjust the salary in other locations) +- Ability to craft your calendar with flexible locations and schedules for many roles +- Unlimited PTO and sick days + ## Region Anywhere From 3b086ee4fb3deb05d972a971804023adddc3beaf Mon Sep 17 00:00:00 2001 From: omesser Date: Fri, 25 Mar 2022 12:05:28 +0300 Subject: [PATCH 24/25] M --- company-profiles/iterative.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/company-profiles/iterative.md b/company-profiles/iterative.md index c2dfc7aa..bcae126b 100644 --- a/company-profiles/iterative.md +++ b/company-profiles/iterative.md @@ -23,11 +23,11 @@ AI teams face challenges that require new technologies. We build these technolog We value great collaboration and communication skills, both among internal teams and in how we interact with our users. We take care to balance and be responsive to the needs of our open source community as well as our enterprise customers. -### WHAT WE OFFER +*WHAT WE OFFER* 🌎 Team is distributed remotely worldwide. -🤗 ****Open source-first company - your work will be visible and will be used by thousands of developers every day! This feels great! Check out our [Discord](http://dvc.org/chat) and [GitHub](http://github.com/iterative). +🤗 Open source-first company - your work will be visible and will be used by thousands of developers every day! This feels great! Check out our [Discord](http://dvc.org/chat) and [GitHub](http://github.com/iterative). ⚓️ Engineering team is involved in product discussions and planning. We do it openly via GitHub or Discord chat. Well-defined process that we all participate in improving. @@ -37,7 +37,7 @@ We value great collaboration and communication skills, both among internal teams 🛡️ 401K with 100% match up to 4% of annual salary (US only, but can discuss and reimburse, adjust the salary in other locations) -### WE TAKE CARE OF OUR PEOPLE +*WE TAKE CARE OF OUR PEOPLE* As a distributed company, diversity drives our identity. Whether you’re looking to launch a new career or grow an existing one, *ITERATIVE* is the type of company where you can balance great work with great life. Your age is only a number. It doesn’t matter if you’re just out of college or your children are; we need you for what you can do. From 339eaeeca848e338d4c89ca9ad34b2bac5430455 Mon Sep 17 00:00:00 2001 From: omesser Date: Tue, 29 Mar 2022 16:37:33 +0300 Subject: [PATCH 25/25] Adjusting lever source label --- company-profiles/iterative.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/company-profiles/iterative.md b/company-profiles/iterative.md index bcae126b..7347ca40 100644 --- a/company-profiles/iterative.md +++ b/company-profiles/iterative.md @@ -62,4 +62,4 @@ The team is 100% remote ## How to apply -Job openings can be found [here](https://jobs.lever.co/iterative?lever-origin=applied&lever-source%5B%5D=gh.remoteintech) +Job openings can be found [here](https://jobs.lever.co/iterative?lever-origin=applied&lever-source%5B%5D=remoteintech)