Skip to content

Commit d8a529a

Browse files
committed
Add processing options
1 parent 4c51085 commit d8a529a

File tree

14 files changed

+8103
-15
lines changed

14 files changed

+8103
-15
lines changed

README.md

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Issue Forms Parser
2+
3+
![Check dist/](https://github.com/github/issue-parser/actions/workflows/check-dist.yml/badge.svg)
4+
![Code Coverage](./badges/coverage.svg)
5+
![CodeQL](https://github.com/github/issue-parser/actions/workflows/codeql.yml/badge.svg)
6+
![Continuous Integration](https://github.com/github/issue-parser/actions/workflows/continuous-integration.yml/badge.svg)
7+
![Continuous Delivery](https://github.com/github/issue-parser/actions/workflows/continuous-delivery.yml/badge.svg)
8+
![Linter](https://github.com/github/issue-parser/actions/workflows/linter.yml/badge.svg)
9+
10+
Convert issue form responses to JSON
11+
12+
## About
13+
14+
This package can be used to parse GitHub issues into machine-readable JSON for
15+
processing. In particular, it is designed to work with
16+
[issue forms](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms)
17+
to perform basic transformations on the issue body, resulting in a consistent
18+
JSON output.
19+
20+
Issues submitted using issue forms use a structured Markdown format. **So long
21+
as the issue body is not heavily modified by the user,** we can reliably parse
22+
the issue body into a JSON object.
23+
24+
## Installation
25+
26+
```bash
27+
npm i @github/issue-parser
28+
```
29+
30+
## Example
31+
32+
Here is a simple example of how to use this package in your project.
33+
34+
```typescript
35+
import { parseIssue } from '@github/issue-parser'
36+
37+
const issue = parseIssue('<issue body>', '<template>')
38+
```
39+
40+
Assuming the issue and template look like these examples:
41+
42+
- [Example Issue Markdown](./__fixtures__/example/issue.md)
43+
- [Example Template YAML](./__fixtures__/example/template.yml)
44+
45+
The resulting `issue` object will look like the following:
46+
47+
```typescript
48+
{
49+
name: 'this-thing',
50+
nickname: 'thing',
51+
color: ['blue'],
52+
shape: ['square'],
53+
sounds: ['re', 'mi'],
54+
topics: [],
55+
description: "This is a description.\n\nIt has multiple lines.\n\nIt's pretty cool!",
56+
notes: '- Note\n- Another note\n- Lots of notes',
57+
code: 'const thing = new Thing()\nthing.doThing()',
58+
'code-string': 'thing.toString()',
59+
'is-thing': {
60+
selected: ['Yes'],
61+
unselected: ['No']
62+
},
63+
'is-thing-useful': {
64+
selected: ['Sometimes'],
65+
unselected: ['Yes', 'No']
66+
},
67+
'read-team': 'IssueOps-Demo-Readers',
68+
'write-team': 'IssueOps-Demo-Writers'
69+
}
70+
```
71+
72+
## Usage
73+
74+
### `parseIssue`
75+
76+
This is the main function of the package. It takes two arguments:
77+
78+
- `issue: string` - The body of the issue to be parsed
79+
- `template: string` - (Optional) The issue form template used to create the
80+
issue
81+
- `options?: { slugify?: boolean }` - (Optional) Additional parsing options to
82+
use when processing the issue body
83+
84+
If the `template` value is provided, the package will attempt to transform the
85+
issue response values into different types based on the `type` property of the
86+
specific field in the template. If the `template` value is omitted, all parsed
87+
values will be returned as strings. For information on the transformations that
88+
are applied, see the [Transformations](#transformations) section.
89+
90+
#### Parsing Options
91+
92+
- `slugify: boolean` - If set to `true`, any parsed keys that are not found in
93+
the issue forms template (if provided) will be converted to
94+
[slugs](https://en.wikipedia.org/wiki/Clean_URL#Slug) using the
95+
[slugify](https://www.npmjs.com/package/slugify) package. Otherwise, the
96+
original header value will be used as the object key.
97+
98+
### `parseTemplate(template?: string)`
99+
100+
Parses an issue form template and returns an object. This can be used to match
101+
form responses in the issue body with the fields, so that you can perform
102+
additional validation.
103+
104+
When parsing an issue and the associated form template, this package will
105+
attempt to match field IDs with response values. If a match is found, the field
106+
ID will be used in the parsed object keys (instead of the header value from the
107+
markdown response). If no match is found, the header text is used as the object
108+
key.
109+
110+
For example, if you have the following issue form template:
111+
112+
```yaml
113+
name: Example Request
114+
description: Submit an example request
115+
title: '[Request] Example'
116+
117+
body:
118+
- type: input
119+
id: name
120+
attributes:
121+
label: The Name of the Thing
122+
description: The name of the thing you want to create.
123+
placeholder: this-is-the-thing
124+
validations:
125+
required: true
126+
```
127+
128+
And the following issue body:
129+
130+
```markdown
131+
### The Name of the Thing
132+
133+
this-thing
134+
135+
### The Nickname of the Thing
136+
137+
thing
138+
```
139+
140+
The resulting parsed issue would be:
141+
142+
```jsonc
143+
{
144+
// Uses the ID value from the issue form template as the key
145+
"name": "this-thing",
146+
// Uses the original header value from the issue body
147+
"The Nickname of the Thing": "thing"
148+
}
149+
```
150+
151+
## Transformations
152+
153+
The following transformations will take place for responses, depending on the
154+
input type. The type is inferred from the issue form template. For information
155+
on each specific type, see
156+
[Syntax for GitHub's form schema](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema).
157+
158+
### Single Line
159+
160+
[Type: `input`](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#input)
161+
162+
Before:
163+
164+
```plain
165+
This is a response
166+
```
167+
168+
After (no change):
169+
170+
```plain
171+
This is a response
172+
```
173+
174+
### Multiline
175+
176+
[Type: `textarea`](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#textarea)
177+
178+
> [!NOTE]
179+
>
180+
> Empty lines are preserved in multiline responses.
181+
182+
Before:
183+
184+
```plain
185+
First line :D
186+
187+
Third line!
188+
```
189+
190+
After:
191+
192+
```plain
193+
First line :D\n\nThird line!
194+
```
195+
196+
### Dropdown Selections
197+
198+
[Type: `dropdown`](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#dropdown)
199+
200+
Before:
201+
202+
```plain
203+
red, blue, green
204+
```
205+
206+
After:
207+
208+
```json
209+
["red", "blue", "green"]
210+
```
211+
212+
### Checkboxes
213+
214+
[Type: `checkboxes`](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#checkboxes)
215+
216+
Before:
217+
218+
```plain
219+
- [x] Pick me!
220+
- [ ] Don't pick me D:
221+
```
222+
223+
After:
224+
225+
```json
226+
{
227+
"selected": ["Pick me!"],
228+
"unselected": ["Don't pick me D:"]
229+
}
230+
```
231+
232+
## Omitting Inputs
233+
234+
In the following situations, an input will be omitted from the output JSON:
235+
236+
| Scenario | Example |
237+
| --------------- | ----------------------- |
238+
| Invalid Heading | `## This is invalid` |
239+
| Empty Heading | `###` |
240+
| | `This is a value` |
241+
| No Value | `### This is a heading` |
242+
| | |
243+
| | `### This is another` |
244+
| | `This is a value` |
245+
246+
Normally, if a form is submitted with empty field(s), they will be included in
247+
the issue body as one of the following, depending on the input type in the form
248+
template.
249+
250+
| Type | No Response |
251+
| ---------- | --------------- |
252+
| `dropdown` | `None` |
253+
| `input` | `_No response_` |
254+
| `textarea` | `_No response_` |
255+
256+
```markdown
257+
### Dropdown Field
258+
259+
None
260+
261+
### Input or Textarea Field
262+
263+
_No response_
264+
265+
### Checkboxes Field
266+
267+
- [ ] Item A
268+
- [ ] Item B
269+
```
270+
271+
These will be converted to one of the following, based on the type of input
272+
specified in the issue form template:
273+
274+
| Type | Output |
275+
| ------------ | ------------------------------------- |
276+
| `checkboxes` | `{ "selected": [], "unselected": []}` |
277+
| `dropdown` | `[]` |
278+
| `input` | `undefined` |
279+
| `textarea` | `undefined` |

__fixtures__/no-template/issue.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### The Name of the Thing
2+
3+
this-thing
4+
5+
### The Nickname of the Thing
6+
7+
thing
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"the_name_of_the_thing": "this-thing",
3+
"the_nickname_of_the_thing": "thing"
4+
}

__tests__/format.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('formatValue()', () => {
5757
type: 'input',
5858
required: true
5959
})
60-
).toBe('')
60+
).toBeUndefined()
6161
})
6262

6363
it('Handles None', () => {

__tests__/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ describe('parseIssue()', () => {
100100
const result = parseIssue(issue, template)
101101
expect(result).toEqual(expected)
102102
})
103+
104+
it('Parses an issue without a corresponding template', () => {
105+
const issue = fs.readFileSync('__fixtures__/no-template/issue.md', 'utf8')
106+
107+
const expected = JSON.parse(
108+
fs.readFileSync('__fixtures__/no-template/parsed-issue.json', 'utf8')
109+
)
110+
111+
const result = parseIssue(issue, undefined, { slugify: true })
112+
expect(result).toEqual(expected)
113+
})
103114
})
104115

105116
describe('parseTemplate()', () => {

dist/enums.d.ts

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/format.d.ts

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.d.ts

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)