@zentered/issue-forms-body-parser 1.2.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parse.js CHANGED
@@ -5,139 +5,88 @@ import remarkParse from 'remark-parse'
5
5
  import remarkGfm from 'remark-gfm'
6
6
  import slugify from '@sindresorhus/slugify'
7
7
  import remarkStringify from 'remark-stringify'
8
- import { parse, isMatch } from 'date-fns'
9
8
  import stripFinalNewline from 'strip-final-newline'
10
9
 
11
- // if the system time is not UTC, we need to convert it to UTC
12
- import { zonedTimeToUtc, formatInTimeZone } from 'date-fns-tz/esm'
13
- const loc = 'UTC'
10
+ import {
11
+ parseDate,
12
+ parseTime,
13
+ parseDuration,
14
+ parseList
15
+ } from './parsers/index.js'
14
16
 
15
- const commonDateFormats = [
16
- 'yyyy-MM-dd',
17
- 'dd/MM/yyyy',
18
- 'dd/MM/yy',
19
- 'dd-MM-yyyy',
20
- 'dd-MM-yy',
21
- 'dd.MM.yyyy',
22
- 'dd.MM.yy'
23
- ]
17
+ export default async function parseMD(body) {
18
+ const tokens = await unified().use(remarkParse).use(remarkGfm).parse(body)
19
+ if (!tokens) {
20
+ return []
21
+ }
24
22
 
25
- const commonTimeFormats = ['HH:mm', 'HH.mm', 'hh:mm a', 'hh:mm A']
23
+ const structuredResponse = {}
24
+ let currentHeading = null
25
+ for (const token of tokens.children) {
26
+ const text = await unified()
27
+ .use(remarkGfm)
28
+ .use(remarkStringify)
29
+ .stringify(token)
30
+ const cleanText = stripFinalNewline(text)
26
31
 
27
- function parseDuration(text) {
28
- const duration = {
29
- hours: 0,
30
- minutes: 0
31
- }
32
+ // issue forms uses h3 as a heading
33
+ if (token.type === 'heading' && token.depth === 3) {
34
+ currentHeading = slugify(token.children[0].value)
35
+ structuredResponse[currentHeading] = {
36
+ title: token.children[0].value,
37
+ content: []
38
+ }
39
+ } else if (token.type === 'paragraph' && currentHeading) {
40
+ const obj = structuredResponse[currentHeading]
32
41
 
33
- const pieces = text.replace('m', '').split('h')
34
- duration.hours = parseInt(pieces[0]) ? parseInt(pieces[0]) : 0
35
- duration.minutes = parseInt(pieces[1]) ? parseInt(pieces[1]) : 0
36
- return duration
37
- }
42
+ const date = parseDate(cleanText)
43
+ const time = parseTime(cleanText)
44
+ const duration = parseDuration(cleanText)
38
45
 
39
- function parseDate(text) {
40
- const match = commonDateFormats.map((format) => {
41
- return isMatch(text, format)
42
- })
43
- if (match.indexOf(true) > -1) {
44
- const date = zonedTimeToUtc(
45
- parse(text, commonDateFormats[match.indexOf(true)], new Date()),
46
- loc
47
- ).toJSON()
48
- return date.split('T')[0]
49
- } else {
50
- return null
51
- }
52
- }
46
+ if (date) {
47
+ obj.date = date
48
+ }
53
49
 
54
- function parseTime(text) {
55
- const match = commonTimeFormats.map((format) => {
56
- return isMatch(text, format)
57
- })
58
- if (match.indexOf(true) > -1) {
59
- const time = zonedTimeToUtc(
60
- parse(text, commonTimeFormats[match.indexOf(true)], new Date()),
61
- loc
62
- )
63
- return formatInTimeZone(time, loc, 'HH:mm')
64
- } else {
65
- return null
66
- }
67
- }
50
+ if (time) {
51
+ obj.time = time
52
+ }
68
53
 
69
- function parseList(list) {
70
- return list.children
71
- .map((item) => {
72
- const listItem = {}
73
- if (item.type === 'list') {
74
- return parseList(list)
75
- } else if (item.type === 'listItem') {
76
- listItem.checked = item.checked
77
- return item.children
78
- .map((child) => {
79
- if (child.type === 'paragraph') {
80
- listItem.text = child.children
81
- .map((c) => {
82
- if (c.type === 'link') {
83
- return c.children[0].value
84
- } else {
85
- return c.value
86
- }
87
- })
88
- .filter((x) => !!x)
89
- .join('')
90
- return listItem
91
- }
92
- })
93
- .filter((x) => !!x)
54
+ if (duration) {
55
+ obj.duration = duration
94
56
  }
95
- })
96
- .filter((x) => !!x)
97
- }
98
57
 
99
- export default async function parseMD(body) {
100
- const tokens = await unified().use(remarkParse).use(remarkGfm).parse(body)
101
- if (!tokens) {
102
- return []
58
+ obj.content.push(cleanText)
59
+ } else if (token.type === 'list') {
60
+ const obj = structuredResponse[currentHeading]
61
+ obj.text = cleanText
62
+ obj.list = parseList(token).flat()
63
+ } else if (token.type === 'html') {
64
+ const obj = structuredResponse[currentHeading]
65
+ obj.content.push(token.html)
66
+ } else if (token.type === 'code') {
67
+ const obj = structuredResponse[currentHeading]
68
+ obj.lang = token.lang
69
+ obj.text = cleanText
70
+ } else if (token.type === 'heading' && token.depth > 3) {
71
+ const obj = structuredResponse[currentHeading]
72
+ obj.content.push(token.children[0].value)
73
+ } else {
74
+ console.log('unhandled token type')
75
+ console.log(token)
76
+ }
103
77
  }
104
78
 
105
- const r = []
106
- for (let idx = 0; idx < tokens.children.length; idx = idx + 2) {
107
- const current = tokens.children[idx]
108
- const hasNext = idx + 1 < tokens.children.length
109
-
110
- if (current.type === 'heading') {
111
- // issue-form answers start with a h3 heading, ignore everything else
112
- const obj = {
113
- id: slugify(current.children[0].value),
114
- title: current.children[0].value
115
- }
116
- if (hasNext) {
117
- const next = tokens.children[idx + 1]
118
- if (next.type === 'list') {
119
- obj.list = parseList(next).flat()
120
- }
121
- const text = await unified()
122
- .use(remarkGfm)
123
- .use(remarkStringify)
124
- .stringify(next)
125
- obj.text = stripFinalNewline(text)
126
- const date = parseDate(obj.text)
127
- const time = parseTime(obj.text)
128
- if (date) {
129
- obj.date = date
130
- }
131
- if (time) {
132
- obj.time = time
133
- }
134
- if (obj.id === 'duration') {
135
- obj.duration = parseDuration(obj.text)
136
- }
79
+ for (const key in structuredResponse) {
80
+ const token = structuredResponse[key]
81
+ const content = token.content.filter(Boolean)
82
+ if (content && content.length > 0) {
83
+ if (content.length === 1) {
84
+ token.text = content[0]
137
85
  }
138
- r.push(obj)
86
+ token.text = content.join('\n\n')
139
87
  }
88
+ token.content = content
140
89
  }
141
90
 
142
- return r
91
+ return structuredResponse
143
92
  }
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ import { parse, isMatch } from 'date-fns'
4
+ import { zonedTimeToUtc } from 'date-fns-tz/esm'
5
+
6
+ const loc = 'UTC'
7
+ const commonDateFormats = [
8
+ 'yyyy-MM-dd',
9
+ 'dd/MM/yyyy',
10
+ 'dd/MM/yy',
11
+ 'dd-MM-yyyy',
12
+ 'dd-MM-yy',
13
+ 'dd.MM.yyyy',
14
+ 'dd.MM.yy'
15
+ ]
16
+
17
+ export default function parseDate(text) {
18
+ const match = commonDateFormats.map((format) => {
19
+ return isMatch(text, format)
20
+ })
21
+ if (match.indexOf(true) > -1) {
22
+ const date = zonedTimeToUtc(
23
+ parse(text, commonDateFormats[match.indexOf(true)], new Date()),
24
+ loc
25
+ ).toJSON()
26
+ return date.split('T')[0]
27
+ } else {
28
+ return null
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ export default function parseDuration(text) {
4
+ let matched = false
5
+ const duration = {
6
+ hours: 0,
7
+ minutes: 0
8
+ }
9
+
10
+ const hoursAndMinutes = new RegExp(/([0-9]+)h([0-9]+)m/)
11
+ const hours = new RegExp(/([0-9]+)h/)
12
+
13
+ if (text.match(hoursAndMinutes)) {
14
+ matched = true
15
+ const [, h, m] = text.match(hoursAndMinutes)
16
+ duration.hours = parseInt(h)
17
+ duration.minutes = parseInt(m)
18
+ } else if (text.match(hours)) {
19
+ matched = true
20
+ const [, h] = text.match(hours)
21
+ duration.hours = parseInt(h)
22
+ duration.minutes = 0
23
+ }
24
+
25
+ if (matched) {
26
+ return duration
27
+ } else {
28
+ return null
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ import date from './date.js'
4
+ import time from './time.js'
5
+ import duration from './duration.js'
6
+ import list from './list.js'
7
+
8
+ export const parseDate = date
9
+ export const parseTime = time
10
+ export const parseDuration = duration
11
+ export const parseList = list
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ export default function parseList(list) {
4
+ return list.children
5
+ .map((item) => {
6
+ const listItem = {}
7
+ if (item.type === 'list') {
8
+ return parseList(list)
9
+ } else if (item.type === 'listItem') {
10
+ listItem.checked = item.checked
11
+ return item.children
12
+ .map((child) => {
13
+ if (child.type === 'paragraph') {
14
+ listItem.text = child.children
15
+ .map((c) => {
16
+ if (c.type === 'link') {
17
+ return c.children[0].value
18
+ } else {
19
+ return c.value
20
+ }
21
+ })
22
+ .filter((x) => !!x)
23
+ .join('')
24
+ return listItem
25
+ }
26
+ })
27
+ .filter((x) => !!x)
28
+ }
29
+ })
30
+ .filter((x) => !!x)
31
+ }
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ import { parse, isMatch } from 'date-fns'
4
+ import { zonedTimeToUtc, formatInTimeZone } from 'date-fns-tz/esm'
5
+
6
+ const loc = 'UTC'
7
+ const commonTimeFormats = ['HH:mm', 'HH.mm', 'hh:mm a', 'hh:mm A']
8
+
9
+ export default function parseTime(text) {
10
+ const match = commonTimeFormats.map((format) => {
11
+ return isMatch(text, format)
12
+ })
13
+ if (match.indexOf(true) > -1) {
14
+ const time = zonedTimeToUtc(
15
+ parse(text, commonTimeFormats[match.indexOf(true)], new Date()),
16
+ loc
17
+ )
18
+ return formatInTimeZone(time, loc, 'HH:mm')
19
+ } else {
20
+ return null
21
+ }
22
+ }
@@ -8,84 +8,78 @@ import { join } from 'path'
8
8
  const test = t.test
9
9
 
10
10
  test('parse(md) should parse GitHub Issue Form data into useful, structured data', async (t) => {
11
- const expected = [
12
- {
13
- id: 'event-description',
11
+ const expected = {
12
+ 'event-description': {
14
13
  title: 'Event Description',
15
- text: "Let's meet for coffee and chat about tech, coding, Cyprus and the newly formed\nCDC (Cyprus Developer Community)."
14
+ content: [
15
+ 'Welcome to the CDC - Cyprus Developer Community! Join us for our monthly Larnaka\nmeet & greet event. Meet likeminded people, discuss topics we would like to hear\nabout in upcoming talks, welcome potential speakers, discuss all things tech and\nhave fun!',
16
+ 'Notice with regards to COVID:',
17
+ 'All attendees must follow measures in accordance with Ministry of Health\ndirectives. <https://www.pio.gov.cy/coronavirus/eng>'
18
+ ],
19
+ text: 'Welcome to the CDC - Cyprus Developer Community! Join us for our monthly Larnaka\nmeet & greet event. Meet likeminded people, discuss topics we would like to hear\nabout in upcoming talks, welcome potential speakers, discuss all things tech and\nhave fun!\n\nNotice with regards to COVID:\n\nAll attendees must follow measures in accordance with Ministry of Health\ndirectives. <https://www.pio.gov.cy/coronavirus/eng>'
16
20
  },
17
- {
18
- id: 'location',
21
+ location: {
19
22
  title: 'Location',
23
+ content: [
24
+ '[Cafe Nero Finikoudes, Larnaka](https://goo.gl/maps/Bzjxdeat3BSdsUSVA)'
25
+ ],
20
26
  text: '[Cafe Nero Finikoudes, Larnaka](https://goo.gl/maps/Bzjxdeat3BSdsUSVA)'
21
27
  },
22
- {
23
- id: 'date',
28
+ date: {
24
29
  title: 'Date',
25
- text: '11.03.2022',
26
- date: '2022-03-11'
30
+ content: ['11.03.2022'],
31
+ date: '2022-03-11',
32
+ text: '11.03.2022'
27
33
  },
28
- { id: 'time', title: 'Time', text: '16:00', time: '16:00' },
29
- {
30
- id: 'duration',
34
+ time: { title: 'Time', content: ['16:00'], time: '16:00', text: '16:00' },
35
+ duration: {
31
36
  title: 'Duration',
32
- text: '2h',
33
- duration: { hours: 2, minutes: 0 }
37
+ content: ['2h'],
38
+ duration: { hours: 2, minutes: 0 },
39
+ text: '2h'
34
40
  },
35
- {
36
- id: 'list-item-checked',
41
+ 'list-item-checked': {
37
42
  title: 'List Item Checked',
43
+ content: [],
44
+ text: "* [x] I agree to follow this project's\n [Code of Conduct](https://berlincodeofconduct.org)",
38
45
  list: [
39
46
  {
40
47
  checked: true,
41
48
  text: "I agree to follow this project's\nCode of Conduct"
42
49
  }
43
- ],
44
- text: "* [x] I agree to follow this project's\n [Code of Conduct](https://berlincodeofconduct.org)"
50
+ ]
45
51
  },
46
- {
47
- id: 'list-item-unchecked',
52
+ 'list-item-unchecked': {
48
53
  title: 'List Item Unchecked',
54
+ content: [],
55
+ text: "* [ ] I agree to follow this project's\n [Code of Conduct](https://berlincodeofconduct.org)",
49
56
  list: [
50
57
  {
51
58
  checked: false,
52
59
  text: "I agree to follow this project's\nCode of Conduct"
53
60
  }
54
- ],
55
- text: "* [ ] I agree to follow this project's\n [Code of Conduct](https://berlincodeofconduct.org)"
61
+ ]
56
62
  },
57
- {
58
- id: 'mixed-task-list',
63
+ 'mixed-task-list': {
59
64
  title: 'Mixed Task List',
65
+ content: [],
66
+ text: '* [x] checked\n* [ ] unchecked\n* [x] checked 2\n* [x] checked 3\n* [ ] unchecked 2',
60
67
  list: [
61
68
  { checked: true, text: 'checked' },
62
69
  { checked: false, text: 'unchecked' },
63
70
  { checked: true, text: 'checked 2' },
64
71
  { checked: true, text: 'checked 3' },
65
72
  { checked: false, text: 'unchecked 2' }
66
- ],
67
- text: '* [x] checked\n* [ ] unchecked\n* [x] checked 2\n* [x] checked 3\n* [ ] unchecked 2'
73
+ ]
68
74
  },
69
- {
70
- id: 'complex-list',
71
- title: 'Complex List',
72
- list: [
73
- { checked: null, text: 'one' },
74
- { checked: null, text: 'two' }
75
- ],
76
- text: '* one\n* two\n * three\n * four\n 1. five\n 2. six'
77
- },
78
- {
79
- id: 'repositories',
75
+ repositories: {
80
76
  title: 'Repositories',
77
+ content: [],
78
+ lang: 'csv',
81
79
  text: '```csv\nhttps://example.com/repository-1\nhttps://example.com/repository-2\n```'
82
80
  },
83
- {
84
- id: 'visibility',
85
- title: 'Visibility',
86
- text: 'Internal'
87
- }
88
- ]
81
+ visibility: { title: 'Visibility', content: ['Internal'], text: 'Internal' }
82
+ }
89
83
 
90
84
  const md = await readFile(
91
85
  join(process.cwd(), 'test', 'test-issue-1.md'),
@@ -93,11 +87,12 @@ test('parse(md) should parse GitHub Issue Form data into useful, structured data
93
87
  )
94
88
  const actual = await fn(md)
95
89
  // console.log(JSON.stringify(actual, null, 0))
96
- t.deepEqual(actual, expected)
90
+
91
+ t.same(actual, expected)
97
92
  })
98
93
 
99
94
  test('parse(md) return nothing', async (t) => {
100
- const expected = []
95
+ const expected = {}
101
96
 
102
97
  const md = await readFile(
103
98
  join(process.cwd(), 'test', 'test-issue-2.md'),
@@ -105,5 +100,5 @@ test('parse(md) return nothing', async (t) => {
105
100
  )
106
101
  const actual = await fn(md)
107
102
  // console.log(JSON.stringify(actual, null, 0))
108
- t.deepEqual(actual, expected)
103
+ t.same(actual, expected)
109
104
  })
@@ -1,7 +1,16 @@
1
1
  ### Event Description
2
2
 
3
- Let's meet for coffee and chat about tech, coding, Cyprus and the newly formed
4
- CDC (Cyprus Developer Community).
3
+ <img width="1107" alt="The Brewery Larnaka" src="https://user-images.githubusercontent.com/3581331/162574191-3c023b32-34d9-4035-90bf-7297cdccaf06.png">
4
+
5
+ Welcome to the CDC - Cyprus Developer Community! Join us for our monthly Larnaka
6
+ meet & greet event. Meet likeminded people, discuss topics we would like to hear
7
+ about in upcoming talks, welcome potential speakers, discuss all things tech and
8
+ have fun!
9
+
10
+ #### Notice with regards to COVID:
11
+
12
+ All attendees must follow measures in accordance with Ministry of Health
13
+ directives. https://www.pio.gov.cy/coronavirus/eng
5
14
 
6
15
  ### Location
7
16
 
@@ -37,15 +46,6 @@ CDC (Cyprus Developer Community).
37
46
  - [x] checked 3
38
47
  - [ ] unchecked 2
39
48
 
40
- ### Complex List
41
-
42
- - one
43
- - two
44
- - three
45
- - four
46
- 1. five
47
- 2. six
48
-
49
49
  ### Repositories
50
50
 
51
51
  ```csv