@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/.github/workflows/codeql-analysis.yml +59 -0
- package/README.md +28 -20
- package/dist/index.js +160 -80
- package/dist/parse.cjs +2 -0
- package/dist/parse.cjs.map +1 -0
- package/dist/parse.esm.js +323 -0
- package/dist/parse.esm.js.map +1 -0
- package/dist/parse.modern.js +2 -0
- package/dist/parse.modern.js.map +1 -0
- package/dist/parse.module.js +2 -0
- package/dist/parse.module.js.map +1 -0
- package/dist/parse.umd.js +2 -0
- package/dist/parse.umd.js.map +1 -0
- package/package.json +19 -10
- package/src/index.js +2 -0
- package/src/parse.js +67 -118
- package/src/parsers/date.js +30 -0
- package/src/parsers/duration.js +30 -0
- package/src/parsers/index.js +11 -0
- package/src/parsers/list.js +31 -0
- package/src/parsers/time.js +22 -0
- package/test/parse-issue.test.js +42 -47
- package/test/test-issue-1.md +11 -11
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
parseDate,
|
|
12
|
+
parseTime,
|
|
13
|
+
parseDuration,
|
|
14
|
+
parseList
|
|
15
|
+
} from './parsers/index.js'
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return duration
|
|
37
|
-
}
|
|
42
|
+
const date = parseDate(cleanText)
|
|
43
|
+
const time = parseTime(cleanText)
|
|
44
|
+
const duration = parseDuration(cleanText)
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
86
|
+
token.text = content.join('\n\n')
|
|
139
87
|
}
|
|
88
|
+
token.content = content
|
|
140
89
|
}
|
|
141
90
|
|
|
142
|
-
return
|
|
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
|
+
}
|
package/test/parse-issue.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
date: '2022-03-11'
|
|
30
|
+
content: ['11.03.2022'],
|
|
31
|
+
date: '2022-03-11',
|
|
32
|
+
text: '11.03.2022'
|
|
27
33
|
},
|
|
28
|
-
{
|
|
29
|
-
{
|
|
30
|
-
id: 'duration',
|
|
34
|
+
time: { title: 'Time', content: ['16:00'], time: '16:00', text: '16:00' },
|
|
35
|
+
duration: {
|
|
31
36
|
title: 'Duration',
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
103
|
+
t.same(actual, expected)
|
|
109
104
|
})
|
package/test/test-issue-1.md
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
### Event Description
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|