Skip to content

Commit

Permalink
fix: Broken mailto: links in Markdoc (#506)
Browse files Browse the repository at this point in the history
  • Loading branch information
dogmar authored Aug 7, 2023
1 parent 72b56fd commit 3260636
Show file tree
Hide file tree
Showing 8 changed files with 1,366 additions and 160 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ jobs:
node-version: ${{ steps.engines.outputs.nodeVersion }}
- run: yarn install --immutable
- run: yarn build:storybook
test:
name: Unit test
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: 'Checkout'
uses: actions/checkout@v3
- name: Read Node.js version from package.json
run: echo "nodeVersion=$(node -p "require('./package.json').engines.node")" >> $GITHUB_OUTPUT
id: engines
- name: 'Setup Node'
uses: actions/setup-node@v3
with:
node-version: ${{ steps.engines.outputs.nodeVersion }}
- run: |
yarn install --immutable
- run: yarn test
lint:
name: Lint
runs-on: ubuntu-latest
Expand Down
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"storybook:serve-static": "yarn build:storybook && http-server storybook-static",
"build": "npx tsc --declaration",
"clean": "rimraf storybook-static dist",
"test": "vitest --run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"lint": "prettier --check ./src && eslint . --ext ts,tsx,js,jsx",
"fix": "run-s fix:format fix:js",
"fix:format": "prettier --write --no-error-on-unmatched-pattern ./src",
Expand Down Expand Up @@ -80,11 +84,14 @@
"@storybook/react": "7.0.22",
"@storybook/react-vite": "7.0.22",
"@storybook/testing-library": "0.1.0",
"@testing-library/jest-dom": "5.17.0",
"@types/react-dom": "18.2.4",
"@types/react-transition-group": "4.4.6",
"@types/styled-components": "5.1.26",
"@typescript-eslint/eslint-plugin": "5.59.9",
"@typescript-eslint/parser": "5.59.9",
"@vitest/coverage-v8": "0.34.1",
"@vitest/ui": "0.34.1",
"babel-loader": "9.1.2",
"conventional-changelog-conventionalcommits": "6.1.0",
"eslint": "8.42.0",
Expand All @@ -102,6 +109,7 @@
"http-server": "14.1.1",
"husky": "8.0.3",
"jest-mock": "29.5.0",
"jsdom": "22.1.0",
"lint-staged": "13.2.2",
"npm-run-all": "4.1.5",
"prettier": "2.8.8",
Expand All @@ -112,7 +120,8 @@
"storybook": "7.0.22",
"styled-components": "5.3.11",
"typescript": "4.9.5",
"vite": "4.3.9"
"vite": "4.4.8",
"vitest": "0.34.1"
},
"peerDependencies": {
"@emotion/react": ">=11.11.0",
Expand Down
33 changes: 2 additions & 31 deletions src/markdoc/utils/text.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
export function removeTrailingSlashes(str: unknown) {
if (typeof str !== 'string') {
return str
}

return str.replace(/\/+$/, '')
}
import { isExternalUrl } from '../../utils/urls'

export function isRelativeUrl(str: string) {
return !str.match(/^\/.*$|^[^:/]*?:\/\/.*?$/giu)
}

export function isExternalUrl(url?: string | null) {
if (!url) return false

return url.substr(0, 4) === 'http' || url.substr(0, 2) === '//'
}
export * from '../../utils/urls'

export const stripMdExtension = (url?: string) => {
url = url ?? ''
Expand All @@ -25,14 +11,6 @@ export const stripMdExtension = (url?: string) => {
return url
}

export function getBarePathFromPath(url: string) {
return url.split(/[?#]/)[0]
}

export function isSubrouteOf(route: string, compareRoute: string) {
return route.startsWith(compareRoute)
}

export const providerToProviderName: Record<string, string> = {
GCP: 'GCP',
AWS: 'AWS',
Expand All @@ -43,10 +21,3 @@ export const providerToProviderName: Record<string, string> = {
KUBERNETES: 'Kubernetes',
GENERIC: 'Generic',
}

export function toHtmlId(str: string) {
const id = str.replace(/\W+/g, ' ').trim().replace(/\s/g, '-').toLowerCase()

// make sure the id starts with a letter or underscore
return id.match(/^[A-Za-z]/) ? id : `_${id}`
}
137 changes: 137 additions & 0 deletions src/utils/urls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
getBarePathFromPath,
isExternalUrl,
isRelativeUrl,
isSubrouteOf,
removeTrailingSlashes,
toHtmlId,
} from './urls'

const relativeUrls = [
'',
'a',
'a/',
'abcd',
'abcd/',
'abcd#something-something',
'abcd/#hash.hash',
'deep/path/to/page.html',
'deep/path/to/page.html#hash',
':something',
'#hash-link',
]

const absoluteUrls = [
'/a',
'/a/',
'/abcd',
'/abcd/',
'/abcd#something-something',
'/abcd/#hash.hash',
'/deep/path/to/page.html',
'/deep/path/to/page.html#hash',
]

const externalUrls = [
// Links with protocols
'//google.com',
'http://google.com',
'https://google.com',
'ftp://google.com',
'gopher://google.com',
'HTTP://google.com',
'HTTPS://google.com',
'FTP://google.com',
'GOPHER://google.com',
'234h+-.:something', // Weird, but valid protocol

// Alternative links
'mailto:',
'mailto:[email protected]',
'tel:',
'sms:',
'callto:',
'tel:+1.123.345.6342',
'sms:+1.123.345.6342',
'callto:+1.123.345.6342',
]

describe('URL utils', () => {
it('should detect relative urls', () => {
relativeUrls.forEach((url) => {
expect(isRelativeUrl(url)).toBeTruthy()
})
absoluteUrls.forEach((url) => {
expect(isRelativeUrl(url)).toBeFalsy()
})
externalUrls.forEach((url) => {
expect(isRelativeUrl(url)).toBeFalsy()
})
})

it('should detect external urls', () => {
relativeUrls.forEach((url) => {
expect(isExternalUrl(url)).toBeFalsy()
})
absoluteUrls.forEach((url) => {
expect(isExternalUrl(url)).toBeFalsy()
})
externalUrls.forEach((url) => {
expect(isExternalUrl(url)).toBeTruthy()
})
})

it('should remove trailing slashes', () => {
expect(removeTrailingSlashes(null)).toBe(null)
expect(removeTrailingSlashes(undefined)).toBe(undefined)
expect(removeTrailingSlashes('/')).toBe('')
expect(removeTrailingSlashes('//')).toBe('')
expect(removeTrailingSlashes('///////')).toBe('')
expect(removeTrailingSlashes('/abc/a/')).toBe('/abc/a')
expect(removeTrailingSlashes('/abc/a////')).toBe('/abc/a')
expect(removeTrailingSlashes('/abc////a')).toBe('/abc////a')
expect(removeTrailingSlashes('http://a.b.c/#d')).toBe('http://a.b.c/#d')
})

it('should detect subroutes', () => {
expect(isSubrouteOf('/', '')).toBeTruthy()
expect(isSubrouteOf('/something', '/')).toBeTruthy()
expect(isSubrouteOf('/a/b/cdefg/h/', '/a/b/cdefg/h/')).toBeTruthy()
expect(isSubrouteOf('/a/b/cdefg/h/ijk', '/a/b/cdefg/h/')).toBeTruthy()
expect(
isSubrouteOf('http://google.com/?x=something', 'http://google.com')
).toBeTruthy()

expect(isSubrouteOf('', '/')).toBeFalsy()
expect(isSubrouteOf('https://google.com', 'http://google.com')).toBeFalsy()
})

it('should create valid id attributes', () => {
expect(toHtmlId('some%#$long9 string-with_chars')).toBe(
'some-long9-string-with_chars'
)
expect(toHtmlId('123 numbers')).toBe('_123-numbers')
expect(toHtmlId('')).toBe('')
expect(toHtmlId(' ')).toBe('')
expect(toHtmlId('a')).toBe('a')
expect(toHtmlId(' abc')).toBe('abc')
expect(toHtmlId('-things')).toBe('things')
expect(toHtmlId('_things')).toBe('_things')
expect(toHtmlId('_thiñgs')).toBe('_thi-gs')
expect(toHtmlId('CAPITALS')).toBe('capitals')
})

it('should get paths without url params or hashes', () => {
expect(getBarePathFromPath('')).toBe('')
expect(getBarePathFromPath('abc')).toBe('abc')
expect(getBarePathFromPath('abc//')).toBe('abc//')
expect(getBarePathFromPath('#hash?var=val')).toBe('')
expect(getBarePathFromPath('path#hash?var=val')).toBe('path')
expect(getBarePathFromPath('//path#hash?var=val')).toBe('//path')
expect(getBarePathFromPath('path/#/morepath')).toBe('path/')
expect(getBarePathFromPath('//path/#hash?var=val')).toBe('//path/')
expect(getBarePathFromPath('http://path.com?var=val')).toBe(
'http://path.com'
)
})
})
20 changes: 13 additions & 7 deletions src/utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export function removeTrailingSlashes(str: string | null | undefined) {
}

export function isRelativeUrl(str: string) {
return !str.match(/^\/.*$|^[^:/]*?:\/\/.*?$/giu)
}
if (str.startsWith('/')) {
return false
}

export function isExternalUrl(url?: string | null) {
if (!url) return false
return !isExternalUrl(str)
}

return url.substr(0, 4) === 'http' || url.substr(0, 2) === '//'
export function isExternalUrl(str?: string | null) {
return !!str.match(/^(\/\/|[a-z\d+-.]+?:)/i)
}

export function getBarePathFromPath(url: string) {
Expand All @@ -25,8 +27,12 @@ export function isSubrouteOf(route: string, compareRoute: string) {
}

export function toHtmlId(str: string) {
const id = str.replace(/\W+/g, ' ').trim().replace(/\s/g, '-').toLowerCase()
const id = str
.replace(/\W+/g, ' ') //
.trim()
.replace(/\s/g, '-')
.toLowerCase()

// make sure the id starts with a letter or underscore
return id.match(/^[A-Za-z]/) ? id : `_${id}`
return id.match(/^($|[A-Za-z_])/) ? id : `_${id}`
}
10 changes: 8 additions & 2 deletions tsconfig.eslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"types": ["@types/node"],
"types": ["@types/node", "vitest/globals"],
"noEmit": true,
"allowJs": true
},
"include": ["src/**/*", ".storybook/**/*", "vite.config.ts", ".eslintrc.cjs"]
"include": [
"src/**/*",
".storybook/**/*",
"vite.config.ts",
"vitest.config.ts",
".eslintrc.cjs"
]
}
14 changes: 14 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig, mergeConfig } from 'vitest/config'

import viteConfig from './vite.config'

// https://vitest.dev/config/
export default mergeConfig(
viteConfig,
defineConfig({
test: {
globals: true,
environment: 'jsdom',
},
})
)
Loading

0 comments on commit 3260636

Please sign in to comment.