-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1a43521
Showing
24 changed files
with
11,491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
const config = { | ||
extends: [ | ||
"eslint:recommended", | ||
"plugin:react/recommended", | ||
"prettier", | ||
"prettier/react", | ||
"plugin:compat/recommended" | ||
], | ||
env: { | ||
browser: true, | ||
"jest/globals": true | ||
}, | ||
parserOptions: { | ||
ecmaFeatures: { | ||
jsx: true, | ||
impliedStrict: true | ||
}, | ||
sourceType: "module" | ||
}, | ||
plugins: ["react", "prettier", "react-hooks", "jest"], | ||
parser: "babel-eslint", | ||
rules: { | ||
semi: ["error", "never"], | ||
"arrow-parens": ["error", "as-needed"], | ||
"prettier/prettier": "error", | ||
"react-hooks/rules-of-hooks": "error", | ||
"react-hooks/exhaustive-deps": "error" | ||
}, | ||
settings: { | ||
react: { | ||
version: "16.0" | ||
} | ||
} | ||
} | ||
|
||
module.exports = config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||
|
||
name: CI | ||
|
||
on: | ||
push: | ||
branches: [ master ] | ||
pull_request: | ||
branches: [ master ] | ||
|
||
jobs: | ||
build: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
strategy: | ||
matrix: | ||
node-version: [12.x] | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Use Node.js ${{ matrix.node-version }} | ||
uses: actions/setup-node@v1 | ||
with: | ||
node-version: ${{ matrix.node-version }} | ||
- run: npm ci | ||
- run: npm run build | ||
- run: npm test | ||
env: | ||
CI: true | ||
- run: npm run lint |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"printWidth": 80, | ||
"tabWidth": 4, | ||
"semi": false, | ||
"trailingComma": "none", | ||
"bracketSpacing": true, | ||
"arrowParens": "avoid", | ||
"proseWrap": "never" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
## Dependencies and target environment | ||
|
||
One core goal this library is to deliver a lightweight solution. Therefore, it comes to critically when it comes to user of dependencies and also polyfill usage. | ||
|
||
- `react` should be its only dependencies. | ||
- No polyfill. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# react-interpolate | ||
|
||
[![size](http://img.badgesize.io/https://cdn.jsdelivr.net/gh/Doist/react-interpolate/dist/react-interpolate.min.cjs?compression=gzip)](http://img.badgesize.io/https://cdn.jsdelivr.net/gh/Doist/react-interpolate/dist/react-interpolate.min.cjs?compression=gzip) | ||
[![Actions Status](https://github.com/Doist/react-interpolate/workflows/CI/badge.svg)](https://github.com/Doist/react-interpolate/actions) | ||
|
||
|
||
A string interpolation component that formats and interpolates a template string in a safe way. | ||
|
||
```jsx | ||
import Interpolate from "@doist/react-interpolate" | ||
|
||
function Greeting() { | ||
return <Interpolate | ||
string="<h1>Hello {name}. Here is <a>your order info</a></h1>" | ||
mapping={ | ||
a={child => <a href="https://orderinfo.com">{child}</a>) | ||
name="William" | ||
} | ||
/> | ||
} | ||
``` | ||
Would render the following HTML | ||
```html | ||
<h1>Hello William. Here is <a href="https://orderinfo.com">your order info</a></h1> | ||
``` | ||
## Component API | ||
`<Interpolate>` component accepts the following props | ||
#### `string` | ||
The template string to be interpolated. Required. | ||
Please see the [Interpolation syntax](./#interpolation-syntax) section below for more detail. | ||
#### `mapping` | ||
An object that defines the values to be injected for placeholder and tags defined in the template string. Optional. | ||
- You can map placeholder and self-closing tag to any [valid element value](https://reactjs.org/docs/react-api.html#isvalidelement) | ||
- For open & close tag, you need to supply a renderer function that defines how the the enclosed children should be rendered. | ||
```jsx | ||
<Interpolate | ||
string="Hello {name}. Here is <a>your order info</a><hr/>" | ||
mapping={ | ||
// you can map placholder and self-closing tag to any valid element value | ||
name="William" | ||
hr={<hr className="break"/>} | ||
|
||
// mapping value for open & close tag must be a function | ||
a={children => <a href="https://orderinfo.com">{children}</a>) | ||
} | ||
/> | ||
``` | ||
#### `graceful` | ||
A boolean flag indicates how string syntax error or mapping error should be handled. When true, the raw string value from the prop `string` would be rendered as a fallback in the error scenario. When false, error would be thrown instead. | ||
Optional. `true` by default. | ||
```jsx | ||
// would render "an invalid string with unclose tag <h1>" | ||
<Interpolate | ||
graceful | ||
string="an invalid string with unclose tag <h1>" | ||
/> | ||
``` | ||
## Interpolation syntax | ||
Here is interpolation syntax you can use in your `string`. | ||
#### Placeholder | ||
```jsx | ||
"hello {user_name}" | ||
``` | ||
Placeholder name should be alphanumeric (`[A-Za-z0-9_]`). Placeholders could be mapped to any valid element value. | ||
#### Open & close tags | ||
```jsx | ||
"Here is <a>your order info</a>" | ||
|
||
// tag name could be any alphanumeric string | ||
"Here is <link>your order info</link>" | ||
|
||
// you can nest tag and placeholder | ||
"Here is <a><b>you order info {name}</b></a>" | ||
``` | ||
Tag name should be alphanumeric (`[A-Za-z0-9_]`). Open & close tag could only be mapped to a renderer function. | ||
```jsx | ||
<Interpolate | ||
string="Here is <a>your order info</a>" | ||
mapping={ | ||
a={children => <a href="https://orderinfo.com">{children}</a>) | ||
} | ||
/> | ||
``` | ||
Unclosed tag or incorrect nesting of tag would result in syntax error. | ||
```js | ||
// bad: no close tag | ||
"Here is <a>your order info" | ||
|
||
// bad: incorrect tag structure | ||
"Here is <a><b>your order info</a></b>" | ||
``` | ||
#### Self closing tag | ||
```js | ||
"Hello.<br/>Here is your order" | ||
``` | ||
Tag name should be alphanumeric (`[A-Za-z0-9_]`). Self closing tags could be mapped to any valid element value. | ||
## Auto tag element creation | ||
When tags are used the string but there are no correponding mapped value, it would by default create the corresponding HTML element by default. | ||
```jsx | ||
// would render: <h1>Hellow</h1><br/>World | ||
<Interpolate string="<h1>Hello</h1><br/>world"/> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
/* eslint-disable react/display-name */ | ||
import React from "react" | ||
import { render } from "@testing-library/react" | ||
import Interpolate from "../src/interpolate" | ||
|
||
Interpolate.defaultProps = { | ||
graceful: false | ||
} | ||
|
||
const surpressConsole = () => { | ||
const w = jest.spyOn(console, "warn").mockImplementation() | ||
const e = jest.spyOn(console, "error").mockImplementation() | ||
|
||
return () => { | ||
w.mockRestore() | ||
e.mockRestore() | ||
} | ||
} | ||
|
||
describe("Interpolate", () => { | ||
function renderTest({ expected, ...props }) { | ||
return () => { | ||
const { container } = render(<Interpolate {...props} />) | ||
expect(container.innerHTML).toEqual(expected) | ||
} | ||
} | ||
|
||
test("when no mapping is provide", () => { | ||
const restore = surpressConsole() // Interpolate will output warning when no mapping is provided | ||
|
||
renderTest({ | ||
string: "<h1>hello <b>{name}</b></h1><br/>. welcome to todoist", | ||
expected: "<h1>hello <b>{name}</b></h1><br>. welcome to todoist" | ||
}) | ||
|
||
restore() | ||
}) | ||
|
||
test( | ||
"tag mapping", | ||
renderTest({ | ||
string: "<h1>hello <b>steven</b></h1>. welcome to todoist", | ||
mapping: { | ||
b: child => <i>{child}</i>, | ||
h1: child => <h2>{child}</h2> | ||
}, | ||
expected: "<h2>hello <i>steven</i></h2>. welcome to todoist" | ||
}) | ||
) | ||
|
||
test( | ||
"placholder mapping", | ||
renderTest({ | ||
string: "{greeting} <b>{name}</b>. welcome to todoist", | ||
mapping: { | ||
greeting: "hi", | ||
name: () => <i>steven</i> | ||
}, | ||
expected: "hi <b><i>steven</i></b>. welcome to todoist" | ||
}) | ||
) | ||
|
||
test( | ||
"void tag mapping", | ||
renderTest({ | ||
string: "hello <br/>", | ||
mapping: { | ||
br: <hr /> | ||
}, | ||
expected: "hello <hr>" | ||
}) | ||
) | ||
|
||
test( | ||
"combination of mapping", | ||
renderTest({ | ||
string: "<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist", | ||
mapping: { | ||
h1: child => <h2>{child}</h2>, | ||
b: child => <i>{child}</i>, | ||
name: "steven", | ||
br: <hr /> | ||
}, | ||
expected: "<h2>hello <i>steven</i></h2>.<hr> welcome to todoist" | ||
}) | ||
) | ||
|
||
test("combination of mapping with function component", () => { | ||
// eslint-disable-next-line | ||
const Subheader = ({ children }) => { | ||
return <h2 className="subheader">{children}</h2> | ||
} | ||
|
||
renderTest({ | ||
string: "<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist", | ||
mapping: { | ||
h1: child => <Subheader>{child}</Subheader>, | ||
b: child => <i>{child}</i>, | ||
name: "steven", | ||
br: <hr /> | ||
}, | ||
expected: | ||
'<h2 class="subheader">hello <i>steven</i></h2>.<hr> welcome to todoist' | ||
}) | ||
}) | ||
|
||
test( | ||
"spacing in the void tag and placeholder should be allowed", | ||
renderTest({ | ||
string: "hello { name }<br /> welcome to todoist", | ||
mapping: { | ||
name: "steven", | ||
br: <hr /> | ||
}, | ||
expected: "hello steven<hr> welcome to todoist" | ||
}) | ||
) | ||
|
||
test("the mapping value should be interpolate corrected with proper html escape", () => { | ||
renderTest({ | ||
string: "hello { name }<br /> welcome to todoist", | ||
mapping: { | ||
name: "<script>window.xss = 1</script>", | ||
br: "<script>window.xss = 1</script>" | ||
}, | ||
expected: | ||
"hello <script>window.xss = 1</script><script>window.xss = 1</script> welcome to todoist" | ||
})() | ||
|
||
expect(window.css).toBeUndefined() | ||
}) | ||
|
||
test("when graceful flag is on and string contains syntax error, interpolate should return the original string and should not throw error", () => { | ||
const restore = surpressConsole() | ||
|
||
renderTest({ | ||
string: "</h1>", | ||
expected: "</h1>", | ||
graceful: true | ||
})() | ||
|
||
restore() | ||
}) | ||
}) | ||
|
||
describe("Interpolate: error", () => { | ||
let restore | ||
beforeAll(() => { | ||
restore = surpressConsole() | ||
}) | ||
afterAll(() => { | ||
restore() | ||
}) | ||
|
||
function renderTest({ props }) { | ||
return () => { | ||
expect(() => render(<Interpolate {...props} />)).toThrow() | ||
} | ||
} | ||
|
||
test("non-closing tag", renderTest({ string: "<b>" })) | ||
|
||
test( | ||
"mapping value for tag should always be a function", | ||
renderTest({ | ||
string: "<h1>hello</h1>. welcome to todoist", | ||
mapping: { h1: "hi" } | ||
}) | ||
) | ||
}) |
Oops, something went wrong.