gwchq-textjam 0.2.10 → 0.2.11
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/dist/index.js +54 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -68344,8 +68344,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
68344
68344
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
68345
68345
|
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
68346
68346
|
/* harmony export */ });
|
|
68347
|
-
/* harmony import */ var
|
|
68348
|
-
/* harmony import */ var
|
|
68347
|
+
/* harmony import */ var D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(89379);
|
|
68348
|
+
/* harmony import */ var D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectWithoutProperties_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(53986);
|
|
68349
68349
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(51649);
|
|
68350
68350
|
/* harmony import */ var _hello_pangea_dnd__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(98850);
|
|
68351
68351
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14062);
|
|
@@ -68369,7 +68369,7 @@ var DraggableTab = _ref => {
|
|
|
68369
68369
|
panelIndex,
|
|
68370
68370
|
fileIndex
|
|
68371
68371
|
} = _ref,
|
|
68372
|
-
otherProps = (0,
|
|
68372
|
+
otherProps = (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectWithoutProperties_js__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A)(_ref, _excluded);
|
|
68373
68373
|
var openFiles = (0,react_redux__WEBPACK_IMPORTED_MODULE_1__.useSelector)(state => state.editor.openedFiles);
|
|
68374
68374
|
var openFilesCount = openFiles[panelIndex].length;
|
|
68375
68375
|
var dispatch = (0,react_redux__WEBPACK_IMPORTED_MODULE_1__.useDispatch)();
|
|
@@ -68386,7 +68386,7 @@ var DraggableTab = _ref => {
|
|
|
68386
68386
|
switchToFileTab(panelIndex, (fileIndex + openFilesCount - 1) % openFilesCount);
|
|
68387
68387
|
}
|
|
68388
68388
|
};
|
|
68389
|
-
var InnerTab = () => /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)(react_tabs__WEBPACK_IMPORTED_MODULE_2__.Tab, (0,
|
|
68389
|
+
var InnerTab = () => /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)(react_tabs__WEBPACK_IMPORTED_MODULE_2__.Tab, (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)((0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)({
|
|
68390
68390
|
onClick: e => {
|
|
68391
68391
|
e.stopPropagation();
|
|
68392
68392
|
switchToFileTab(panelIndex, fileIndex);
|
|
@@ -68404,7 +68404,7 @@ var DraggableTab = _ref => {
|
|
|
68404
68404
|
draggableProps,
|
|
68405
68405
|
dragHandleProps
|
|
68406
68406
|
} = _ref2;
|
|
68407
|
-
return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("div", (0,
|
|
68407
|
+
return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("div", (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)((0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)((0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)({
|
|
68408
68408
|
className: "draggable-tab",
|
|
68409
68409
|
ref: innerRef
|
|
68410
68410
|
}, draggableProps), dragHandleProps), {}, {
|
|
@@ -68426,8 +68426,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
68426
68426
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
68427
68427
|
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
68428
68428
|
/* harmony export */ });
|
|
68429
|
-
/* harmony import */ var
|
|
68430
|
-
/* harmony import */ var
|
|
68429
|
+
/* harmony import */ var D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(89379);
|
|
68430
|
+
/* harmony import */ var D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectWithoutProperties_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(53986);
|
|
68431
68431
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(51649);
|
|
68432
68432
|
/* harmony import */ var _hello_pangea_dnd__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(98850);
|
|
68433
68433
|
/* harmony import */ var react_tabs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(39243);
|
|
@@ -68446,8 +68446,8 @@ var DroppableTabList = _ref => {
|
|
|
68446
68446
|
children: _children,
|
|
68447
68447
|
index
|
|
68448
68448
|
} = _ref,
|
|
68449
|
-
otherProps = (0,
|
|
68450
|
-
return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_3__.jsx)(react_tabs__WEBPACK_IMPORTED_MODULE_1__.TabList, (0,
|
|
68449
|
+
otherProps = (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectWithoutProperties_js__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A)(_ref, _excluded);
|
|
68450
|
+
return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_3__.jsx)(react_tabs__WEBPACK_IMPORTED_MODULE_1__.TabList, (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A)((0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A)({}, otherProps), {}, {
|
|
68451
68451
|
children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_3__.jsx)(_hello_pangea_dnd__WEBPACK_IMPORTED_MODULE_6__.Droppable, {
|
|
68452
68452
|
direction: "horizontal",
|
|
68453
68453
|
droppableId: index.toString(),
|
|
@@ -68457,7 +68457,7 @@ var DroppableTabList = _ref => {
|
|
|
68457
68457
|
droppableProps,
|
|
68458
68458
|
placeholder
|
|
68459
68459
|
} = _ref2;
|
|
68460
|
-
return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxs)("div", (0,
|
|
68460
|
+
return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxs)("div", (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A)((0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A)({
|
|
68461
68461
|
className: "droppable-tab-list"
|
|
68462
68462
|
}, droppableProps), {}, {
|
|
68463
68463
|
ref: innerRef,
|
|
@@ -103011,7 +103011,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
103011
103011
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
103012
103012
|
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
103013
103013
|
/* harmony export */ });
|
|
103014
|
-
/* harmony import */ var
|
|
103014
|
+
/* harmony import */ var D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(89379);
|
|
103015
103015
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(51649);
|
|
103016
103016
|
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14062);
|
|
103017
103017
|
/* harmony import */ var _redux_EditorSlice__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(68512);
|
|
@@ -103091,7 +103091,7 @@ var ProjectName = _ref => {
|
|
|
103091
103091
|
id: "project_name_label",
|
|
103092
103092
|
className: _styles_module_scss__WEBPACK_IMPORTED_MODULE_3__["default"].projectLabel,
|
|
103093
103093
|
children: "Project Name"
|
|
103094
|
-
}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxs)("div", (0,
|
|
103094
|
+
}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxs)("div", (0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)((0,D_gwc_gwchq_textjam_node_modules_babel_runtime_helpers_esm_objectSpread2_js__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A)({
|
|
103095
103095
|
className: classnames__WEBPACK_IMPORTED_MODULE_2___default()(_styles_module_scss__WEBPACK_IMPORTED_MODULE_3__["default"].projectName, className)
|
|
103096
103096
|
}, hoverProps), {}, {
|
|
103097
103097
|
children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)((components_Tooltip_Tooltip__WEBPACK_IMPORTED_MODULE_8___default()), {
|
|
@@ -369195,7 +369195,7 @@ function HtmlConsole({ consoleLogs, }) {
|
|
|
369195
369195
|
return ((0, jsx_runtime_1.jsx)("pre", { className: (0, classnames_1.default)(styles_module_scss_1.default.console, styles_module_scss_1.default.webConsole), children: consoleLogs.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { children: "No logs received yet" })) : (consoleLogs.map((log, i) => {
|
|
369196
369196
|
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.webConsoleLine, styles_module_scss_1.default[log.method]), children: [(0, jsx_runtime_1.jsxs)("span", { children: [(log.method === "error" || log.method === "warn") && ((0, jsx_runtime_1.jsx)(SvgIcon_1.SvgIcon, { SvgElement: log.method === "error" ? alert_svg_1.default : alertTriangle_svg_1.default, size: 12, className: log.method === "error"
|
|
369197
369197
|
? styles_module_scss_1.default.errorIcon
|
|
369198
|
-
: styles_module_scss_1.default.warnIcon })), log.timestamp && ((0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.timestamp, children: log.timestamp }))] }), log.data?.map((node, idx) => ((0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(LogRenderer_1.default, { node: node }), log.data && idx < log.data.length - 1 && " "] }, idx)))] }, i));
|
|
369198
|
+
: styles_module_scss_1.default.warnIcon })), log.timestamp && ((0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.timestamp, children: log.timestamp }))] }), (0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.consoleMessageContent, children: (0, jsx_runtime_1.jsxs)("span", { className: styles_module_scss_1.default.consoleMessageInline, children: [log.data?.map((node, idx) => ((0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(LogRenderer_1.default, { node: node }), log.data && idx < log.data.length - 1 && " "] }, idx))), log.count && log.count > 1 && ((0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.consoleCount, children: log.count }))] }) })] }, i));
|
|
369199
369199
|
})) }));
|
|
369200
369200
|
}
|
|
369201
369201
|
exports["default"] = HtmlConsole;
|
|
@@ -372495,7 +372495,7 @@ const WebComponentProject = ({ nameEditable = false, sidebarOptions = [], packag
|
|
|
372495
372495
|
return () => clearTimeout(timeout);
|
|
372496
372496
|
}, [project]);
|
|
372497
372497
|
renderer.link = function (data) {
|
|
372498
|
-
return `<a href="${data.href}" target="_blank" rel="noreferrer"
|
|
372498
|
+
return `<a href="${data.href}" target="_blank" rel="noreferrer"
|
|
372499
372499
|
}">${data.text}</a>`;
|
|
372500
372500
|
};
|
|
372501
372501
|
marked_1.marked.setOptions({
|
|
@@ -373511,6 +373511,7 @@ const useProject = ({ projectData, projectContent = null, isContentLoaded = null
|
|
|
373511
373511
|
const isPreview = (0, stores_1.useAppSelector)((state) => state.editor.isOutputOnly);
|
|
373512
373512
|
const commitIdLoadTriggered = (0, stores_1.useAppSelector)((state) => state.editor.commitIdLoadTriggered);
|
|
373513
373513
|
const isLoadingCommit = (0, stores_1.useAppSelector)((state) => state.editor.isLoadingCommit);
|
|
373514
|
+
const saveTriggered = (0, stores_1.useAppSelector)((state) => state.editor.saveTriggered);
|
|
373514
373515
|
const isMounted = (0, useIsMounted_1.useIsMounted)();
|
|
373515
373516
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
373516
373517
|
const cacheKey = project.identifier ?? projectData?.identifier;
|
|
@@ -373585,15 +373586,11 @@ const useProject = ({ projectData, projectContent = null, isContentLoaded = null
|
|
|
373585
373586
|
}
|
|
373586
373587
|
// if user has chosen start new for a draft, ignore cache completely
|
|
373587
373588
|
if (!projectData?.commitId && shouldBypassCachedDraft) {
|
|
373588
|
-
if (
|
|
373589
|
-
setProjectDataFromTemplate(projectData);
|
|
373590
|
-
dispatch((0, EditorSlice_1.setCommits)({ commits: projectData?.commits ?? [] }));
|
|
373591
|
-
}
|
|
373592
|
-
else {
|
|
373589
|
+
if (loadTemplateProjectData && !isTemplateDataLoaded) {
|
|
373593
373590
|
dispatch((0, EditorSlice_1.setLoading)(types_1.LoadingState.IDLE));
|
|
373594
|
-
loadTemplateProjectData
|
|
373591
|
+
loadTemplateProjectData();
|
|
373592
|
+
return;
|
|
373595
373593
|
}
|
|
373596
|
-
return;
|
|
373597
373594
|
}
|
|
373598
373595
|
// same draft project - show modal before normal cache flow
|
|
373599
373596
|
if (shouldShowUnsavedDraftModal && cachedProject) {
|
|
@@ -373608,8 +373605,16 @@ const useProject = ({ projectData, projectContent = null, isContentLoaded = null
|
|
|
373608
373605
|
dispatch((0, EditorSlice_1.setCommits)({ commits: projectData?.commits ?? [] }));
|
|
373609
373606
|
return;
|
|
373610
373607
|
}
|
|
373608
|
+
const isSaveInProgressOrSettling = saveTriggered ||
|
|
373609
|
+
savingState === types_1.SavingState.PROCESS ||
|
|
373610
|
+
savingState === types_1.SavingState.SUCCESS;
|
|
373611
|
+
if (isSaveInProgressOrSettling) {
|
|
373612
|
+
return;
|
|
373613
|
+
}
|
|
373611
373614
|
// if no local data found or cache should not be used, and commitId provided, try to load from commit
|
|
373612
|
-
if ((hasNoData || !shouldUseCache) &&
|
|
373615
|
+
if ((hasNoData || !shouldUseCache) &&
|
|
373616
|
+
projectData?.commitId &&
|
|
373617
|
+
project.commitId !== projectData.commitId) {
|
|
373613
373618
|
// if we need to load content, but the loading is failed, set failed state and stop
|
|
373614
373619
|
if (isContentLoaded === false) {
|
|
373615
373620
|
dispatch((0, EditorSlice_1.setLoading)(types_1.LoadingState.FAILED));
|
|
@@ -373650,6 +373655,9 @@ const useProject = ({ projectData, projectContent = null, isContentLoaded = null
|
|
|
373650
373655
|
shouldBypassCachedDraft,
|
|
373651
373656
|
shouldShowUnsavedDraftModal,
|
|
373652
373657
|
openUnsavedDraftModal,
|
|
373658
|
+
project.commitId,
|
|
373659
|
+
saveTriggered,
|
|
373660
|
+
savingState,
|
|
373653
373661
|
]);
|
|
373654
373662
|
// Load commit data when commitIdLoadTriggered is set
|
|
373655
373663
|
(0, react_1.useEffect)(() => {
|
|
@@ -373821,6 +373829,7 @@ const useProjectPersistence = ({ user, projectData, hasShownSavePrompt, saveProj
|
|
|
373821
373829
|
const justLoaded = (0, stores_1.useAppSelector)((state) => state.editor.justLoaded);
|
|
373822
373830
|
const leaveFlow = (0, stores_1.useAppSelector)((state) => state.leaveFlow);
|
|
373823
373831
|
const codeRunTriggered = (0, stores_1.useAppSelector)((state) => state.editor.codeRunTriggered);
|
|
373832
|
+
const existingCommits = (0, stores_1.useAppSelector)((state) => state.editor.commits);
|
|
373824
373833
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
373825
373834
|
const { deleteValueFromCache, upsertCacheValue } = (0, useProjectCache_1.useProjectCache)();
|
|
373826
373835
|
const handleSave = (0, react_1.useCallback)(async () => {
|
|
@@ -373867,7 +373876,21 @@ const useProjectPersistence = ({ user, projectData, hasShownSavePrompt, saveProj
|
|
|
373867
373876
|
}));
|
|
373868
373877
|
return;
|
|
373869
373878
|
}
|
|
373870
|
-
const
|
|
373879
|
+
const incomingCommits = projectData.commits ?? [];
|
|
373880
|
+
const commitsById = new Map();
|
|
373881
|
+
[...existingCommits, ...incomingCommits].forEach((commit) => {
|
|
373882
|
+
if (!commit?.id)
|
|
373883
|
+
return;
|
|
373884
|
+
commitsById.set(commit.id, {
|
|
373885
|
+
...commitsById.get(commit.id),
|
|
373886
|
+
...commit,
|
|
373887
|
+
});
|
|
373888
|
+
});
|
|
373889
|
+
const commits = Array.from(commitsById.values()).sort((a, b) => {
|
|
373890
|
+
const aTime = new Date(a.createdAt || 0).getTime();
|
|
373891
|
+
const bTime = new Date(b.createdAt || 0).getTime();
|
|
373892
|
+
return bTime - aTime;
|
|
373893
|
+
});
|
|
373871
373894
|
// first save: new commitId incoming, but no commitId were set before
|
|
373872
373895
|
const isFirstSave = commits.length === 1 && !project.commitId && projectData.commitId;
|
|
373873
373896
|
// commit different: new commitId incoming, but another commitId is set before
|
|
@@ -373895,6 +373918,8 @@ const useProjectPersistence = ({ user, projectData, hasShownSavePrompt, saveProj
|
|
|
373895
373918
|
hasStructureChanges: false,
|
|
373896
373919
|
components: cleanedComponents,
|
|
373897
373920
|
identifier: projectData.identifier,
|
|
373921
|
+
commitId: projectData.commitId,
|
|
373922
|
+
commits,
|
|
373898
373923
|
};
|
|
373899
373924
|
const newLastSavedSnapshot = (0, buildProjectSnapshot_1.buildProjectSnapshot)(cleanedProject);
|
|
373900
373925
|
const updatedProjectSnapshot = {
|
|
@@ -373920,6 +373945,7 @@ const useProjectPersistence = ({ user, projectData, hasShownSavePrompt, saveProj
|
|
|
373920
373945
|
saving,
|
|
373921
373946
|
leaveFlow.requestId,
|
|
373922
373947
|
leaveFlow.status,
|
|
373948
|
+
existingCommits,
|
|
373923
373949
|
]);
|
|
373924
373950
|
(0, react_1.useEffect)(() => {
|
|
373925
373951
|
if (!saveTriggered || saving !== types_1.SavingState.IDLE)
|
|
@@ -374109,6 +374135,11 @@ const useUnsavedDraftResolution = ({ projectData, cachedProject, isCurrentOutdat
|
|
|
374109
374135
|
// flag to ignore cached draft after Start New
|
|
374110
374136
|
const [shouldBypassCachedDraft, setShouldBypassCachedDraft] = (0, react_1.useState)(false);
|
|
374111
374137
|
const isSameDraftType = (0, react_1.useMemo)(() => isSameDraftProjectType(projectData, cachedProject), [projectData, cachedProject]);
|
|
374138
|
+
(0, react_1.useEffect)(() => {
|
|
374139
|
+
if (projectData?.commitId && shouldBypassCachedDraft) {
|
|
374140
|
+
setShouldBypassCachedDraft(false);
|
|
374141
|
+
}
|
|
374142
|
+
}, [projectData?.commitId, shouldBypassCachedDraft]);
|
|
374112
374143
|
// show modal only for matching draft when cache is valid and not bypassed
|
|
374113
374144
|
const shouldShowUnsavedDraftModal = !!cachedProject &&
|
|
374114
374145
|
!isCurrentOutdated &&
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gwchq-textjam",
|
|
3
3
|
"description": "Embeddable React editor used in Raspberry Pi text-based projects.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.11",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/GirlsFirst/gwchq-textjam",
|
|
7
7
|
"author": "Girls Who Code HQ",
|