@zentered/issue-forms-body-parser 1.2.0 → 1.4.0

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.
@@ -0,0 +1,59 @@
1
+ name: "CodeQL"
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ # The branches below must be a subset of the branches above
8
+ branches: [ main ]
9
+ schedule:
10
+ - cron: '29 21 * * 5'
11
+
12
+ jobs:
13
+ analyze:
14
+ name: Analyze
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ actions: read
18
+ contents: read
19
+ security-events: write
20
+
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ language: [ 'javascript' ]
25
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
26
+ # Learn more about CodeQL language support at https://git.io/codeql-language-support
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v3
31
+
32
+ # Initializes the CodeQL tools for scanning.
33
+ - name: Initialize CodeQL
34
+ uses: github/codeql-action/init@v2
35
+ with:
36
+ languages: ${{ matrix.language }}
37
+ # If you wish to specify custom queries, you can do so here or in a config file.
38
+ # By default, queries listed here will override any specified in a config file.
39
+ # Prefix the list here with "+" to use these queries and those in the config file.
40
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
41
+
42
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
43
+ # If this step fails, then you should remove it and run the build manually (see below)
44
+ - name: Autobuild
45
+ uses: github/codeql-action/autobuild@v2
46
+
47
+ # ℹ️ Command-line programs to run using the OS shell.
48
+ # 📚 https://git.io/JvXDl
49
+
50
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
51
+ # and modify them (or add more) to build your code if your project
52
+ # uses a compiled language
53
+
54
+ #- run: |
55
+ # make bootstrap
56
+ # make release
57
+
58
+ - name: Perform CodeQL Analysis
59
+ uses: github/codeql-action/analyze@v2
package/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  ![Test](https://github.com/zentered/issue-forms-body-parser/workflows/Test/badge.svg)
4
4
  ![Release](https://github.com/zentered/issue-forms-body-parser/workflows/Publish/badge.svg)
5
- ![Semantic Release](https://github.com/zentered/issue-forms-body-parser/workflows/Semantic%20Release/badge.svg)
6
5
  [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
7
6
  [![zentered.co](https://img.shields.io/badge/%3E-zentered.co-blue.svg?style=flat)](https://zentered.co)
8
7
 
@@ -21,9 +20,9 @@ and provides structured data to create calendar entries (ie `.ics` files for
21
20
  [calendar subscriptions with GitEvents](https://github.com/gitevents/ics)),
22
21
  calling 3rd party APIs, etc.
23
22
 
24
- \_Inspired by:
23
+ _Inspired by:
25
24
  [Peter Murray's Issue Forms Body Parser](https://github.com/peter-murray/issue-forms-body-parser)
26
- with valuable feedback from [Steffen](https://gist.github.com/steffen)\_
25
+ with valuable feedback from [Steffen](https://gist.github.com/steffen)_
27
26
 
28
27
  ## Features
29
28
 
@@ -62,30 +61,47 @@ Cafe Nero Finikoudes, Larnaka
62
61
  to structured, usable data:
63
62
 
64
63
  ```json
65
- [
66
- {
67
- "id": "event-description",
64
+ {
65
+ "event-description": {
66
+ "order": 0,
68
67
  "title": "Event Description",
69
- "text": "Let's meet for coffee and chat about tech, coding, Cyprus and the newly formed\nCDC (Cyprus Developer Community).\n"
68
+ "text": "Let's meet for coffee and chat about tech, coding, Cyprus and the newly formed\nCDC (Cyprus Developer Community)."
70
69
  },
71
- {
72
- "id": "location",
70
+ "location": {
71
+ "order": 1,
73
72
  "title": "Location",
74
- "text": "Cafe Nero Finikoudes, Larnaka\n"
73
+ "text": "Cafe Nero Finikoudes, Larnaka"
75
74
  },
76
- {
77
- "id": "date",
75
+ "date": {
76
+ "order": 2,
78
77
  "title": "Date",
79
- "text": "11.03.2022\n",
80
- "date": "2022-03-11T00:00:00.000Z"
78
+ "text": "11.03.2022",
79
+ "date": "2022-03-11"
81
80
  },
82
- { "id": "time", "title": "Time", "text": "16:00\n", "time": "16:00" }
83
- ]
81
+ "time": {
82
+ "order": 3,
83
+ "title": "Time",
84
+ "text": "16:00",
85
+ "time": "16:00"
86
+ }
87
+ }
84
88
  ```
85
89
 
86
90
  See more examples in [md test cases](./test/test-issue-1.md) and
87
91
  [test results](./test/parse-issue-test.md]).
88
92
 
93
+ ### Parsers
94
+
95
+ - `date`: checks if the value matches a
96
+ [common date format](https://github.com/zentered/issue-forms-body-parser/blob/main/src/parsers/date.js#L7)
97
+ and returns a formatted `date` field (in UTC).
98
+ - `time`: checks if the value matches a
99
+ [common time format](https://github.com/zentered/issue-forms-body-parser/blob/main/src/parsers/time.js#L7)
100
+ and returns a formatted `time` field.
101
+ - `lists`: automatically returns lists as arrays
102
+ - `duration`: currently only the format `XXhYYm` is supported as duration, ie.
103
+ `1h30m` returns a `duration` object with `hours` and `minutes`.
104
+
89
105
  ## Installation & Usage
90
106
 
91
107
  ### GitHub Actions
@@ -101,7 +117,7 @@ jobs:
101
117
  steps:
102
118
  - name: Issue Forms Body Parser
103
119
  id: parse
104
- uses: zentered/issue-forms-body-parser@1.0.0
120
+ uses: zentered/issue-forms-body-parser@v2.0.0
105
121
  - run: echo "${{ JSON.stringify(steps.parse.outputs.data) }}"
106
122
  ```
107
123
 
package/dist/index.js CHANGED
@@ -51787,6 +51787,22 @@ function remarkStringify(options) {
51787
51787
 
51788
51788
  /* harmony default export */ const remark_stringify = (remarkStringify);
51789
51789
 
51790
+ ;// CONCATENATED MODULE: ./node_modules/strip-final-newline/index.js
51791
+ function stripFinalNewline(input) {
51792
+ const LF = typeof input === 'string' ? '\n' : '\n'.charCodeAt();
51793
+ const CR = typeof input === 'string' ? '\r' : '\r'.charCodeAt();
51794
+
51795
+ if (input[input.length - 1] === LF) {
51796
+ input = input.slice(0, -1);
51797
+ }
51798
+
51799
+ if (input[input.length - 1] === CR) {
51800
+ input = input.slice(0, -1);
51801
+ }
51802
+
51803
+ return input;
51804
+ }
51805
+
51790
51806
  // EXTERNAL MODULE: ./node_modules/date-fns/index.js
51791
51807
  var date_fns = __nccwpck_require__(3314);
51792
51808
  // EXTERNAL MODULE: ./node_modules/date-fns/_lib/cloneObject/index.js
@@ -52566,6 +52582,38 @@ function zonedTimeToUtc(date, timeZone, options) {
52566
52582
  return new Date(utc + offsetMilliseconds)
52567
52583
  }
52568
52584
 
52585
+ ;// CONCATENATED MODULE: ./src/parsers/date.js
52586
+
52587
+
52588
+ ;
52589
+
52590
+
52591
+ const loc = 'UTC'
52592
+ const commonDateFormats = [
52593
+ 'yyyy-MM-dd',
52594
+ 'dd/MM/yyyy',
52595
+ 'dd/MM/yy',
52596
+ 'dd-MM-yyyy',
52597
+ 'dd-MM-yy',
52598
+ 'dd.MM.yyyy',
52599
+ 'dd.MM.yy'
52600
+ ]
52601
+
52602
+ function date_parseDate(text) {
52603
+ const match = commonDateFormats.map((format) => {
52604
+ return (0,date_fns.isMatch)(text, format)
52605
+ })
52606
+ if (match.indexOf(true) > -1) {
52607
+ const date = zonedTimeToUtc(
52608
+ (0,date_fns.parse)(text, commonDateFormats[match.indexOf(true)], new Date()),
52609
+ loc
52610
+ ).toJSON()
52611
+ return date.split('T')[0]
52612
+ } else {
52613
+ return null
52614
+ }
52615
+ }
52616
+
52569
52617
  // EXTERNAL MODULE: ./node_modules/date-fns/format/index.js
52570
52618
  var format = __nccwpck_require__(2168);
52571
52619
  ;// CONCATENATED MODULE: ./node_modules/date-fns-tz/esm/_lib/tzIntlTimeZoneName/index.js
@@ -53178,74 +53226,65 @@ function formatInTimeZone(date, timeZone, formatStr, options) {
53178
53226
  return format_format(utcToZonedTime(date, timeZone), formatStr, extendedOptions)
53179
53227
  }
53180
53228
 
53181
- ;// CONCATENATED MODULE: ./src/parse.js
53229
+ ;// CONCATENATED MODULE: ./src/parsers/time.js
53182
53230
 
53183
53231
 
53184
53232
  ;
53185
53233
 
53186
53234
 
53235
+ const time_loc = 'UTC'
53236
+ const commonTimeFormats = ['HH:mm', 'HH.mm', 'hh:mm a', 'hh:mm A']
53187
53237
 
53238
+ function time_parseTime(text) {
53239
+ const match = commonTimeFormats.map((format) => {
53240
+ return (0,date_fns.isMatch)(text, format)
53241
+ })
53242
+ if (match.indexOf(true) > -1) {
53243
+ const time = zonedTimeToUtc(
53244
+ (0,date_fns.parse)(text, commonTimeFormats[match.indexOf(true)], new Date()),
53245
+ time_loc
53246
+ )
53247
+ return formatInTimeZone(time, time_loc, 'HH:mm')
53248
+ } else {
53249
+ return null
53250
+ }
53251
+ }
53188
53252
 
53253
+ ;// CONCATENATED MODULE: ./src/parsers/duration.js
53189
53254
 
53190
53255
 
53191
- // if the system time is not UTC, we need to convert it to UTC
53192
-
53193
- const loc = 'UTC'
53194
-
53195
- const commonDateFormats = [
53196
- 'yyyy-MM-dd',
53197
- 'dd/MM/yyyy',
53198
- 'dd/MM/yy',
53199
- 'dd-MM-yyyy',
53200
- 'dd-MM-yy',
53201
- 'dd.MM.yyyy',
53202
- 'dd.MM.yy'
53203
- ]
53204
-
53205
- const commonTimeFormats = ['HH:mm', 'HH.mm', 'hh:mm a', 'hh:mm A']
53206
-
53207
53256
  function parseDuration(text) {
53257
+ let matched = false
53208
53258
  const duration = {
53209
53259
  hours: 0,
53210
53260
  minutes: 0
53211
53261
  }
53212
53262
 
53213
- const pieces = text.replace('m', '').split('h')
53214
- duration.hours = parseInt(pieces[0]) ? parseInt(pieces[0]) : 0
53215
- duration.minutes = parseInt(pieces[1]) ? parseInt(pieces[1]) : 0
53216
- return duration
53217
- }
53263
+ const hoursAndMinutes = new RegExp(/([0-9]+)h([0-9]+)m/)
53264
+ const hours = new RegExp(/([0-9]+)h/)
53218
53265
 
53219
- function parse_parseDate(text) {
53220
- const match = commonDateFormats.map((format) => {
53221
- return (0,date_fns.isMatch)(text, format)
53222
- })
53223
- if (match.indexOf(true) > -1) {
53224
- const date = zonedTimeToUtc(
53225
- (0,date_fns.parse)(text, commonDateFormats[match.indexOf(true)], new Date()),
53226
- loc
53227
- ).toJSON()
53228
- return date.split('T')[0]
53229
- } else {
53230
- return null
53266
+ if (text.match(hoursAndMinutes)) {
53267
+ matched = true
53268
+ const [, h, m] = text.match(hoursAndMinutes)
53269
+ duration.hours = parseInt(h)
53270
+ duration.minutes = parseInt(m)
53271
+ } else if (text.match(hours)) {
53272
+ matched = true
53273
+ const [, h] = text.match(hours)
53274
+ duration.hours = parseInt(h)
53275
+ duration.minutes = 0
53231
53276
  }
53232
- }
53233
53277
 
53234
- function parse_parseTime(text) {
53235
- const match = commonTimeFormats.map((format) => {
53236
- return (0,date_fns.isMatch)(text, format)
53237
- })
53238
- if (match.indexOf(true) > -1) {
53239
- const time = zonedTimeToUtc(
53240
- (0,date_fns.parse)(text, commonTimeFormats[match.indexOf(true)], new Date()),
53241
- loc
53242
- )
53243
- return formatInTimeZone(time, loc, 'HH:mm')
53278
+ if (matched) {
53279
+ return duration
53244
53280
  } else {
53245
53281
  return null
53246
53282
  }
53247
53283
  }
53248
53284
 
53285
+ ;// CONCATENATED MODULE: ./src/parsers/list.js
53286
+
53287
+
53249
53288
  function parseList(list) {
53250
53289
  return list.children
53251
53290
  .map((item) => {
@@ -53276,54 +53315,113 @@ function parseList(list) {
53276
53315
  .filter((x) => !!x)
53277
53316
  }
53278
53317
 
53318
+ ;// CONCATENATED MODULE: ./src/parsers/index.js
53319
+
53320
+
53321
+ ;
53322
+
53323
+
53324
+
53325
+
53326
+ const parsers_parseDate = date_parseDate
53327
+ const parsers_parseTime = time_parseTime
53328
+ const parsers_parseDuration = parseDuration
53329
+ const parsers_parseList = parseList
53330
+
53331
+ ;// CONCATENATED MODULE: ./src/parse.js
53332
+
53333
+
53334
+ ;
53335
+
53336
+
53337
+
53338
+
53339
+
53340
+
53341
+
53342
+
53279
53343
  async function parseMD(body) {
53280
53344
  const tokens = await unified().use(remark_parse).use(remarkGfm).parse(body)
53281
53345
  if (!tokens) {
53282
53346
  return []
53283
53347
  }
53284
53348
 
53285
- const r = []
53286
- for (let idx = 0; idx < tokens.children.length; idx = idx + 2) {
53287
- const current = tokens.children[idx]
53288
- const hasNext = idx + 1 < tokens.children.length
53349
+ const structuredResponse = {}
53350
+ let currentHeading = null
53351
+ for (const token of tokens.children) {
53352
+ const text = await unified()
53353
+ .use(remarkGfm)
53354
+ .use(remark_stringify)
53355
+ .stringify(token)
53356
+ const cleanText = stripFinalNewline(text)
53357
+
53358
+ // issue forms uses h3 as a heading
53359
+ if (token.type === 'heading' && token.depth === 3) {
53360
+ currentHeading = slugify(token.children[0].value)
53361
+ structuredResponse[currentHeading] = {
53362
+ title: token.children[0].value,
53363
+ content: []
53364
+ }
53365
+ } else if (token.type === 'paragraph' && currentHeading) {
53366
+ const obj = structuredResponse[currentHeading]
53367
+
53368
+ const date = parsers_parseDate(cleanText)
53369
+ const time = parsers_parseTime(cleanText)
53370
+ const duration = parsers_parseDuration(cleanText)
53289
53371
 
53290
- if (current.type === 'heading') {
53291
- // issue-form answers start with a h3 heading, ignore everything else
53292
- const obj = {
53293
- id: slugify(current.children[0].value),
53294
- title: current.children[0].value
53372
+ if (date) {
53373
+ obj.date = date
53295
53374
  }
53296
- if (hasNext) {
53297
- const next = tokens.children[idx + 1]
53298
- if (next.type === 'list') {
53299
- obj.list = parseList(next).flat()
53300
- }
53301
- obj.text = await unified()
53302
- .use(remarkGfm)
53303
- .use(remark_stringify)
53304
- .stringify(next)
53305
- const date = parse_parseDate(obj.text)
53306
- const time = parse_parseTime(obj.text)
53307
- if (date) {
53308
- obj.date = date
53309
- }
53310
- if (time) {
53311
- obj.time = time
53312
- }
53313
- if (obj.id === 'duration') {
53314
- obj.duration = parseDuration(obj.text)
53315
- }
53375
+
53376
+ if (time) {
53377
+ obj.time = time
53378
+ }
53379
+
53380
+ if (duration) {
53381
+ obj.duration = duration
53316
53382
  }
53317
- r.push(obj)
53383
+
53384
+ obj.content.push(cleanText)
53385
+ } else if (token.type === 'list') {
53386
+ const obj = structuredResponse[currentHeading]
53387
+ obj.text = cleanText
53388
+ obj.list = parsers_parseList(token).flat()
53389
+ } else if (token.type === 'html') {
53390
+ const obj = structuredResponse[currentHeading]
53391
+ obj.content.push(token.html)
53392
+ } else if (token.type === 'code') {
53393
+ const obj = structuredResponse[currentHeading]
53394
+ obj.lang = token.lang
53395
+ obj.text = cleanText
53396
+ } else if (token.type === 'heading' && token.depth > 3) {
53397
+ const obj = structuredResponse[currentHeading]
53398
+ obj.content.push(token.children[0].value)
53399
+ } else {
53400
+ console.log('unhandled token type')
53401
+ console.log(token)
53318
53402
  }
53319
53403
  }
53320
53404
 
53321
- return r
53405
+ for (const key in structuredResponse) {
53406
+ const token = structuredResponse[key]
53407
+ const content = token.content.filter(Boolean)
53408
+ if (content && content.length > 0) {
53409
+ if (content.length === 1) {
53410
+ token.text = content[0]
53411
+ }
53412
+ token.text = content.join('\n\n')
53413
+ }
53414
+ token.content = content
53415
+ }
53416
+
53417
+ return structuredResponse
53322
53418
  }
53323
53419
 
53324
53420
  ;// CONCATENATED MODULE: ./src/index.js
53325
53421
 
53326
53422
 
53423
+ ;
53424
+
53327
53425
 
53328
53426
 
53329
53427
  async function run() {
package/dist/licenses.txt CHANGED
@@ -1429,6 +1429,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1429
1429
  THE SOFTWARE.
1430
1430
 
1431
1431
 
1432
+ strip-final-newline
1433
+ MIT
1434
+ MIT License
1435
+
1436
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
1437
+
1438
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1439
+
1440
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1441
+
1442
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1443
+
1444
+
1432
1445
  tr46
1433
1446
  MIT
1434
1447
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zentered/issue-forms-body-parser",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "description": "Parser for GitHub Issue Form body, also available as GitHub Action",
6
6
  "keywords": [
@@ -62,6 +62,7 @@
62
62
  "remark-gfm": "^3.0.1",
63
63
  "remark-parse": "^10.0.1",
64
64
  "remark-stringify": "^10.0.2",
65
+ "strip-final-newline": "^3.0.0",
65
66
  "unified": "^10.1.2"
66
67
  },
67
68
  "devDependencies": {
package/src/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  import github from '@actions/github'
2
4
  import core from '@actions/core'
3
5
  import parse from './parse.js'
package/src/parse.js CHANGED
@@ -5,137 +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'
8
+ import stripFinalNewline from 'strip-final-newline'
9
9
 
10
- // if the system time is not UTC, we need to convert it to UTC
11
- import { zonedTimeToUtc, formatInTimeZone } from 'date-fns-tz/esm'
12
- const loc = 'UTC'
10
+ import {
11
+ parseDate,
12
+ parseTime,
13
+ parseDuration,
14
+ parseList
15
+ } from './parsers/index.js'
13
16
 
14
- const commonDateFormats = [
15
- 'yyyy-MM-dd',
16
- 'dd/MM/yyyy',
17
- 'dd/MM/yy',
18
- 'dd-MM-yyyy',
19
- 'dd-MM-yy',
20
- 'dd.MM.yyyy',
21
- 'dd.MM.yy'
22
- ]
17
+ export default async function parseMD(body) {
18
+ const tokens = await unified().use(remarkParse).use(remarkGfm).parse(body)
19
+ if (!tokens) {
20
+ return []
21
+ }
23
22
 
24
- 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)
25
31
 
26
- function parseDuration(text) {
27
- const duration = {
28
- hours: 0,
29
- minutes: 0
30
- }
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]
31
41
 
32
- const pieces = text.replace('m', '').split('h')
33
- duration.hours = parseInt(pieces[0]) ? parseInt(pieces[0]) : 0
34
- duration.minutes = parseInt(pieces[1]) ? parseInt(pieces[1]) : 0
35
- return duration
36
- }
42
+ const date = parseDate(cleanText)
43
+ const time = parseTime(cleanText)
44
+ const duration = parseDuration(cleanText)
37
45
 
38
- function parseDate(text) {
39
- const match = commonDateFormats.map((format) => {
40
- return isMatch(text, format)
41
- })
42
- if (match.indexOf(true) > -1) {
43
- const date = zonedTimeToUtc(
44
- parse(text, commonDateFormats[match.indexOf(true)], new Date()),
45
- loc
46
- ).toJSON()
47
- return date.split('T')[0]
48
- } else {
49
- return null
50
- }
51
- }
46
+ if (date) {
47
+ obj.date = date
48
+ }
52
49
 
53
- function parseTime(text) {
54
- const match = commonTimeFormats.map((format) => {
55
- return isMatch(text, format)
56
- })
57
- if (match.indexOf(true) > -1) {
58
- const time = zonedTimeToUtc(
59
- parse(text, commonTimeFormats[match.indexOf(true)], new Date()),
60
- loc
61
- )
62
- return formatInTimeZone(time, loc, 'HH:mm')
63
- } else {
64
- return null
65
- }
66
- }
50
+ if (time) {
51
+ obj.time = time
52
+ }
67
53
 
68
- function parseList(list) {
69
- return list.children
70
- .map((item) => {
71
- const listItem = {}
72
- if (item.type === 'list') {
73
- return parseList(list)
74
- } else if (item.type === 'listItem') {
75
- listItem.checked = item.checked
76
- return item.children
77
- .map((child) => {
78
- if (child.type === 'paragraph') {
79
- listItem.text = child.children
80
- .map((c) => {
81
- if (c.type === 'link') {
82
- return c.children[0].value
83
- } else {
84
- return c.value
85
- }
86
- })
87
- .filter((x) => !!x)
88
- .join('')
89
- return listItem
90
- }
91
- })
92
- .filter((x) => !!x)
54
+ if (duration) {
55
+ obj.duration = duration
93
56
  }
94
- })
95
- .filter((x) => !!x)
96
- }
97
57
 
98
- export default async function parseMD(body) {
99
- const tokens = await unified().use(remarkParse).use(remarkGfm).parse(body)
100
- if (!tokens) {
101
- 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
+ }
102
77
  }
103
78
 
104
- const r = []
105
- for (let idx = 0; idx < tokens.children.length; idx = idx + 2) {
106
- const current = tokens.children[idx]
107
- const hasNext = idx + 1 < tokens.children.length
108
-
109
- if (current.type === 'heading') {
110
- // issue-form answers start with a h3 heading, ignore everything else
111
- const obj = {
112
- id: slugify(current.children[0].value),
113
- title: current.children[0].value
114
- }
115
- if (hasNext) {
116
- const next = tokens.children[idx + 1]
117
- if (next.type === 'list') {
118
- obj.list = parseList(next).flat()
119
- }
120
- obj.text = await unified()
121
- .use(remarkGfm)
122
- .use(remarkStringify)
123
- .stringify(next)
124
- const date = parseDate(obj.text)
125
- const time = parseTime(obj.text)
126
- if (date) {
127
- obj.date = date
128
- }
129
- if (time) {
130
- obj.time = time
131
- }
132
- if (obj.id === 'duration') {
133
- obj.duration = parseDuration(obj.text)
134
- }
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]
135
85
  }
136
- r.push(obj)
86
+ token.text = content.join('\n\n')
137
87
  }
88
+ token.content = content
138
89
  }
139
90
 
140
- return r
91
+ return structuredResponse
141
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).\n"
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',
20
- text: '[Cafe Nero Finikoudes, Larnaka](https://goo.gl/maps/Bzjxdeat3BSdsUSVA)\n'
23
+ content: [
24
+ '[Cafe Nero Finikoudes, Larnaka](https://goo.gl/maps/Bzjxdeat3BSdsUSVA)'
25
+ ],
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\n',
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\n', 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\n',
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)\n"
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)\n"
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\n'
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\n'
77
- },
78
- {
79
- id: 'repositories',
75
+ repositories: {
80
76
  title: 'Repositories',
81
- text: '```csv\nhttps://example.com/repository-1\nhttps://example.com/repository-2\n```\n'
77
+ content: [],
78
+ lang: 'csv',
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\n'
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