From 64d19a051aeb7d22a0d5758b0d63c247291ecfeb Mon Sep 17 00:00:00 2001 From: caishi <1149225589@qq.com> Date: Thu, 16 Mar 2023 17:04:52 +0800 Subject: [PATCH 01/52] =?UTF-8?q?markdown=20#=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/forge/Issues/Component/newPanel.jsx | 1 + .../tpm/challengesnew/css/newquestion.css | 36 +++++++- .../tpm/challengesnew/tpm-md-editor.js | 83 ++++++++++++++++--- 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/src/forge/Issues/Component/newPanel.jsx b/src/forge/Issues/Component/newPanel.jsx index 0094be5a0..c34c4f64d 100644 --- a/src/forge/Issues/Component/newPanel.jsx +++ b/src/forge/Issues/Component/newPanel.jsx @@ -86,6 +86,7 @@ function NewPanel(props,ref){ onChange={onContentChange} className="mt20" isCanAtme = {true} + isQuoteIssue={true} owner = {owner} projectsId = {projectsId} changeAtWhoLoginList = {changeAtWhoLoginList} diff --git a/src/modules/tpm/challengesnew/css/newquestion.css b/src/modules/tpm/challengesnew/css/newquestion.css index eb1a67eb1..81c4dcdc8 100644 --- a/src/modules/tpm/challengesnew/css/newquestion.css +++ b/src/modules/tpm/challengesnew/css/newquestion.css @@ -5,9 +5,41 @@ color:#5091FF !important; border-color: #5091FF !important; } - +.quoteDiv{ + width: 240px!important; + max-height: 257px!important; +} +.quoteDiv > li{ + display: flex; + align-items: center; + height: 32px; + padding:0px 10px; + border-bottom: 1px solid rgba(212, 212, 212, 0.5); +} +.quoteDiv > li.active{ + background-color: #F3F4F6; +} +.quoteDiv > li:last-child{ + border-bottom: none; +} +.issueIndex{ + background-color: rgba(213, 220, 246, 0.36); + border-radius: 4px; + height: 19px; + line-height: 19px; + color: #666666; + text-align: center; + float: left; + padding: 0px 2px; + cursor: pointer; + font-size: 12px; + margin-right: 7px; +} +.issueName{ + flex:1 +} /*md编辑器中输入@弹出可选人列表样式*/ -.at_who_list{ +.at_who_list,.quoteDiv{ position: absolute; z-index: 100; width: 180px; diff --git a/src/modules/tpm/challengesnew/tpm-md-editor.js b/src/modules/tpm/challengesnew/tpm-md-editor.js index 61747f507..f5d703cff 100644 --- a/src/modules/tpm/challengesnew/tpm-md-editor.js +++ b/src/modules/tpm/challengesnew/tpm-md-editor.js @@ -74,7 +74,7 @@ function md_elocalStorage(editor, mdu, id) { return tid } -export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch, showNullButton = false, showResizeBar = false, startInit = true , forMember = true , isCanAtme = false , changeAtWhoLoginList, owner, projectsId }) => { +export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch, showNullButton = false, showResizeBar = false, startInit = true , forMember = true , isCanAtme = false , isQuoteIssue = false , changeAtWhoLoginList, owner, projectsId }) => { const editorEl = useRef(); const resizeBarEl = useRef(); @@ -91,6 +91,10 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla const editorBodyId = `mdEditors_${mdID}`; const tipId = `e_tips_mdEditor_${mdID}`; + // quote相关 + const [issues, setIssues ] = useState([]); + const [ quoteVisible , setQuoteVisible ] = useState(false); + useEffect(()=>{ //请求members接口获取全部可@列表 isCanAtme && axios.get(`/${owner}/${projectsId}/members.json`).then(response=>{ @@ -104,8 +108,55 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla atWhoVisibleRef.current = false; setAtWhoVisible(false); }) + + isQuoteIssue && getIssueList(); },[]) + // 请求issues接口获取全部可quote的issue列表 + function getIssueList(keywords){ + axios.get(`/v1/${owner}/${projectsId}/issues`,{params:{ + only_name:true,keywords + }}).then(result=>{ + if(result && result.data.total_count!== 0){ + setIssues(result.data.issues); + } + }) + } + // # 引用的弹框div + const QuoteDiv = ( +
]*>/g;
const strRegexSub = /:([a-zA-Z_]+):/g;
-// const quoteRegex = /[#]()/g;
+const quoteRegex = /\[[#][0-9]{0,}\]\(\/\w{0,}\/\w{0,}\/issues\/[0-9]{0,}\)/g;
function _unescape(str) {
let div = document.createElement('div')
div.innerHTML = str
@@ -22,8 +23,29 @@ export default ({
value = '',
className,
style = {},
- url
+ url,
+ owner=undefined,
+ projectsId=undefined
}) => {
+ const [ issues , setIssues ] = useState([]);
+
+ useEffect(()=>{
+ if(owner&&projectsId){
+ getIssueList();
+ }
+ },[owner,projectsId])
+
+ function getIssueList(){
+ axios.get(`/v1/${owner}/${projectsId}/issues`,{params:{
+ only_name:true,sort_direction:"desc",sort_by:"issues.created_on"
+ }}).then(result=>{
+ if(result){
+ let data = result.data.issues;
+ setIssues(data);
+ }
+ })
+ }
+
let str = String(value);
const html = useMemo(() => {
let rs = marked(str);
@@ -39,6 +61,26 @@ export default ({
rs = rs.replace(matchStr[i],getEmoji(matchStr[i]));
}
}
+ if(owner && projectsId && issues && issues.length>0){
+ let matchQuote = str.match(quoteRegex);
+ if(matchQuote && matchQuote.length>0){
+ let getIndexReg = /(?<=#)(.+?)(?=\])/g;
+ for(var x=0;x0 && getIndex[0]){
+ let index = getIndex[0];
+ let filter = issues.filter(f=>f.project_issues_index.toString() === index);
+ if(filter && filter.length === 1){
+ let content = `#${index}:${filter[0].subject}`;
+ rs = rs.replace(`#${index}`,content);
+ }else{
+ let content = `#${index}(已删除)`;
+ rs = rs.replace(`#${index}`,content);
+ }
+ }
+ }
+ }
+ }
rs = rs.replace(/(__special_katext_id_\d+__)/g, (_match, capture) => {
const { type, expression } = math_expressions[capture];
@@ -47,7 +89,7 @@ export default ({
rs = rs.replace(/▁/g, "▁▁▁")
resetMathExpressions()
return dompurify.sanitize(rs)
- }, [str]);
+ }, [str,issues]);
// 锚点跳转,链接地址里含#对应的id
useEffect(()=>{
diff --git a/src/forge/Issues/Component/newPanel.jsx b/src/forge/Issues/Component/newPanel.jsx
index 643ef2348..67a02943f 100644
--- a/src/forge/Issues/Component/newPanel.jsx
+++ b/src/forge/Issues/Component/newPanel.jsx
@@ -6,7 +6,7 @@ import Attachments from "../../Upload/attachment";
import UploadImg from '../Img/UploadImg.png';
function NewPanel(props,ref){
- const [ description , setDescription ] = useState("");
+ const [ description , setDescription ] = useState(undefined);
const [ fileList , setFileList] = useState(undefined);
const [ attachments , setAttachments ] = useState([]);
const [ save , setSave ] = useState([]);
@@ -15,12 +15,11 @@ function NewPanel(props,ref){
const { createFunc , title , desc , files , onCancel , owner , projectsId } = props;
const { form: { getFieldDecorator, validateFields , setFieldsValue } } = props;
-
useState(()=>{
title && setTimeout(()=>{
setFieldsValue({subject:title});
+ desc && onContentChange(desc);
},100)
- desc && setDescription(desc);
files && setAttachments(files);
files && setSave(files);
},[title , desc , files])
@@ -84,7 +83,7 @@ function NewPanel(props,ref){
{})
}else{
@@ -318,6 +321,7 @@ function Details(props){
props.history.push(`/${owner}/${projectsId}/issues/${index}`);
}else{
setEdit(false);
+ window.scrollTo(0,0);
}
}
@@ -367,7 +371,7 @@ function Details(props){
{...props}
onCancel={onCancel}
title={details.subject}
- desc={details.description}
+ desc={edit ? desc : undefined}
files={copy ? undefined : details.attachments}
createFunc={createFunc}
owner = {owner} projectsId = {projectsId}
@@ -405,8 +409,8 @@ function Details(props){
}
]*>/g;
const strRegexSub = /:([a-zA-Z_]+):/g;
-const quoteRegex = /\[[#][0-9]{0,}\]\(\/\w{0,}\/\w{0,}\/issues\/[0-9]{0,}\)/g;
+const quoteRegex = /\[[#][0-9]{0,}\]\(\/(.*?)\/(.*?)\/issues\/[0-9]{0,}\)/g;
function _unescape(str) {
let div = document.createElement('div')
div.innerHTML = str
@@ -63,6 +63,7 @@ export default ({
}
if(owner && projectsId && issues && issues.length>0){
let matchQuote = str.match(quoteRegex);
+ console.log(matchQuote,str,owner,projectsId);
if(matchQuote && matchQuote.length>0){
let getIndexReg = /(?<=#)(.+?)(?=\])/g;
for(var x=0;x
Date: Fri, 14 Apr 2023 15:29:36 +0800
Subject: [PATCH 11/52] =?UTF-8?q?=E6=AD=A3=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/render-html.jsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/components/render-html.jsx b/src/components/render-html.jsx
index e806e7792..8414572b6 100644
--- a/src/components/render-html.jsx
+++ b/src/components/render-html.jsx
@@ -63,7 +63,6 @@ export default ({
}
if(owner && projectsId && issues && issues.length>0){
let matchQuote = str.match(quoteRegex);
- console.log(matchQuote,str,owner,projectsId);
if(matchQuote && matchQuote.length>0){
let getIndexReg = /(?<=#)(.+?)(?=\])/g;
for(var x=0;x
Date: Tue, 18 Apr 2023 14:20:56 +0800
Subject: [PATCH 12/52] replace md alert
---
package-lock.json | 14 ++++++++++++++
package.json | 1 +
src/modules/tpm/challengesnew/tpm-md-editor.js | 13 ++++++++++++-
3 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/package-lock.json b/package-lock.json
index 88b71ed39..f86c5bdd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4561,6 +4561,11 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
+ "cssfilter": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmmirror.com/cssfilter/-/cssfilter-0.0.10.tgz",
+ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
+ },
"cssnano": {
"version": "4.1.11",
"resolved": "http://173.15.15.82:8081/repository/npm-all/cssnano/-/cssnano-4.1.11.tgz",
@@ -21771,6 +21776,15 @@
"resolved": "http://173.15.15.82:8081/repository/npm-all/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
+ "xss": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmmirror.com/xss/-/xss-1.0.14.tgz",
+ "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
+ "requires": {
+ "commander": "^2.20.3",
+ "cssfilter": "0.0.10"
+ }
+ },
"xtend": {
"version": "4.0.2",
"resolved": "http://173.15.15.82:8081/repository/npm-all/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index e94f160af..89c63dd0b 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,7 @@
"styled-components": "^4.4.1",
"whatwg-fetch": "2.0.3",
"wrap-md-editor": "^0.2.20",
+ "xss": "^1.0.14",
"xterm": "4.8.1",
"xterm-addon-fit": "0.4.0"
},
diff --git a/src/modules/tpm/challengesnew/tpm-md-editor.js b/src/modules/tpm/challengesnew/tpm-md-editor.js
index f2901a757..ccd5d83ad 100644
--- a/src/modules/tpm/challengesnew/tpm-md-editor.js
+++ b/src/modules/tpm/challengesnew/tpm-md-editor.js
@@ -493,7 +493,18 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
});
editorInstance.cm.on("change", (cm) => {
//调用父组件的onchange方法,将输入内容传入父级组件
- onChange && onChange(cm.getValue());
+ let reg = /alert\((.*?)\)/g;
+ let v = cm.getValue();
+ if(v){
+ let matchvalue = v.match(reg);
+ if(matchvalue && matchvalue.length>0){
+ for(var x=0;x
Date: Tue, 18 Apr 2023 15:09:13 +0800
Subject: [PATCH 13/52] xss
---
.../tpm/challengesnew/tpm-md-editor.js | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/src/modules/tpm/challengesnew/tpm-md-editor.js b/src/modules/tpm/challengesnew/tpm-md-editor.js
index ccd5d83ad..af89ed0b2 100644
--- a/src/modules/tpm/challengesnew/tpm-md-editor.js
+++ b/src/modules/tpm/challengesnew/tpm-md-editor.js
@@ -1,6 +1,8 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { getUploadActionUrl, getUrl } from 'educoder';
import ResizeObserver from 'resize-observer-polyfill';
+// import myxss from '../../../common/filterXss';
+import xss from 'xss';
import { getImageUrl } from 'educoder';
import axios from 'axios';
import '../../courses/css/Courses.css';
@@ -493,18 +495,18 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
});
editorInstance.cm.on("change", (cm) => {
//调用父组件的onchange方法,将输入内容传入父级组件
- let reg = /alert\((.*?)\)/g;
+ // let reg = /alert\((.*?)\)/g;
let v = cm.getValue();
- if(v){
- let matchvalue = v.match(reg);
- if(matchvalue && matchvalue.length>0){
- for(var x=0;x0){
+ // for(var x=0;x
Date: Tue, 18 Apr 2023 16:43:22 +0800
Subject: [PATCH 14/52] =?UTF-8?q?=E5=8A=A8=E6=80=81=20title?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package-lock.json | 4 ++--
src/forge/Activity/Activity.js | 6 +++---
src/forge/Activity/branchLine.jsx | 4 ++--
src/forge/Activity/issueLine.jsx | 4 ++--
src/forge/Activity/smoothLineTwo.jsx | 4 ++--
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index f86c5bdd2..68c9f20a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19220,7 +19220,7 @@
},
"ssri": {
"version": "7.1.1",
- "resolved": "http://173.15.15.82:8081/repository/npm-all/ssri/-/ssri-7.1.1.tgz",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.1.tgz",
"integrity": "sha512-w+daCzXN89PseTL99MkA+fxJEcU3wfaE/ah0i0lnOlpG1CYLJ2ZjzEry68YBKfLs4JfoTShrTEsJkAZuNZ/stw==",
"dev": true,
"requires": {
@@ -21095,7 +21095,7 @@
},
"y18n": {
"version": "4.0.3",
- "resolved": "http://173.15.15.82:8081/repository/npm-all/y18n/-/y18n-4.0.3.tgz",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
diff --git a/src/forge/Activity/Activity.js b/src/forge/Activity/Activity.js
index 616c94511..956159f7c 100644
--- a/src/forge/Activity/Activity.js
+++ b/src/forge/Activity/Activity.js
@@ -263,15 +263,15 @@ class Activity extends Component{
项目演化分析
- 开源项目社群激发熵
+ 开源项目社群激发演化拓扑熵
基于信息熵围绕疑修任务的社群群智激发演化度量
- 开源项目代码变更熵
+ 开源项目代码变更演化拓扑熵
基于信息熵围绕代码提交的软件代码变更演化度量
- 开源项目社区演化熵
+ 开源项目社区演化拓扑熵
基于信息熵围绕项目社群的社区分裂、缩减、合并和扩大演化行为度量
diff --git a/src/forge/Activity/branchLine.jsx b/src/forge/Activity/branchLine.jsx
index 2fe35e136..9c8b3194c 100644
--- a/src/forge/Activity/branchLine.jsx
+++ b/src/forge/Activity/branchLine.jsx
@@ -41,7 +41,7 @@ function Line({url}) {
yAxis: {
type: 'value',
scale: true,
- name: '社\n群\n激\n发\n熵',
+ name: '社\n群\n激\n发\n演\n化\n拓\n扑\n熵',
min:0,
splitLine : {
show : true,
@@ -49,7 +49,7 @@ function Line({url}) {
nameLocation:'end',
nameTextStyle:{
fontSize:14,
- padding:[0,70,-80,0]
+ padding:[0,70,-120,0]
}
},
dataZoom: [
diff --git a/src/forge/Activity/issueLine.jsx b/src/forge/Activity/issueLine.jsx
index 8baf80f72..5a6a125ef 100644
--- a/src/forge/Activity/issueLine.jsx
+++ b/src/forge/Activity/issueLine.jsx
@@ -43,7 +43,7 @@ function Line({url}) {
yAxis: {
type: 'value',
scale: true,
- name: '代\n码\n变\n更\n熵',
+ name: '代\n码\n变\n更\n演\n化\n拓\n扑\n熵',
min:0,
splitLine : {
show : true,
@@ -51,7 +51,7 @@ function Line({url}) {
nameLocation:'end',
nameTextStyle:{
fontSize:14,
- padding:[0,70,-80,0]
+ padding:[0,70,-120,0]
}
},
dataZoom: [
diff --git a/src/forge/Activity/smoothLineTwo.jsx b/src/forge/Activity/smoothLineTwo.jsx
index fcac40df0..b5dd5b33c 100644
--- a/src/forge/Activity/smoothLineTwo.jsx
+++ b/src/forge/Activity/smoothLineTwo.jsx
@@ -53,7 +53,7 @@ function SmoothLineTwo({url}) {
yAxis: {
type: 'value',
scale: true,
- name: '社\n区\n演\n化\n熵',
+ name: '社\n区\n演\n化\n拓\n扑\n熵',
min:0,
splitLine : {
show : true,
@@ -61,7 +61,7 @@ function SmoothLineTwo({url}) {
nameLocation:'end',
nameTextStyle:{
fontSize:14,
- padding:[0,70,-80,0]
+ padding:[0,70,-120,0]
}
},
dataZoom: [
From cbc34891ca8bab4954848dce0fdd0123377cff95 Mon Sep 17 00:00:00 2001
From: caishi <1149225589@qq.com>
Date: Wed, 19 Apr 2023 11:29:42 +0800
Subject: [PATCH 15/52] =?UTF-8?q?=E8=AE=BE=E7=BD=AE-webhooks=E5=88=86?=
=?UTF-8?q?=E9=A1=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Settings/Webhooks/sub/PushHistory.jsx | 28 +++++++++++++++----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/src/forge/Settings/Webhooks/sub/PushHistory.jsx b/src/forge/Settings/Webhooks/sub/PushHistory.jsx
index b9c6a462b..f375f33f7 100644
--- a/src/forge/Settings/Webhooks/sub/PushHistory.jsx
+++ b/src/forge/Settings/Webhooks/sub/PushHistory.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { Button, Collapse , Tooltip , Spin } from 'antd';
+import { Button, Collapse , Tooltip , Spin , Pagination } from 'antd';
import axios from 'axios';
import Content from './historyContent';
import Fault from '../images/fault.png';
@@ -8,21 +8,30 @@ const { Panel } = Collapse;
function PushHistory({id,owner,projectsId,showNotification}) {
const [ list , setList ] = useState(undefined);
const [ isSpin , setIsSpin ] = useState(false);
+ const [ page , setPage ] = useState(1);
+ const [ total , setTotal ] = useState(0);
+ const pageSize = 10;
useEffect(()=>{
if(id && owner && projectsId){
- Init();
+ Init(1);
}
},[id,owner,projectsId])
- function Init() {
+ useEffect(()=>{
+ setList(undefined)
+ Init(page);
+ },[page])
+
+ function Init(page) {
const url = `/${owner}/${projectsId}/webhooks/${id}/tasks.json`;
axios.get(url,{
- params:{page:1,limit:10}
+ params:{page:page,limit:pageSize}
}).then(result=>{
if(result && result.data){
setList(result.data.tasks);
setIsSpin(false);
+ setTotal(result.data.total_count);
}
}).catch(error=>{})
}
@@ -49,7 +58,7 @@ function PushHistory({id,owner,projectsId,showNotification}) {
{
- list && list.length>0 &&
+ list && list.length>0 ?
{
list.map((i,k)=>{
@@ -75,6 +84,15 @@ function PushHistory({id,owner,projectsId,showNotification}) {
})
}
+ :
+
+
+
+ }
+ {
+ total > pageSize ?
+ setPage(p)}/>
+ :""
}
+ {organizeDetail && organizeDetail.nickname} + {organizeDetail && organizeDetail.is_admin ? + 设置 + :""} +
+=63v!M^x2WV^0(40CX>
zZBsHpp~l1x?+(h^#eT{skrJuPel-D=qkNmmYH<>?SP3fK>TB}PpoxT_Fvdj4JXv
zan$03?O0N`wShMElK5IY2s%LBdny2fe-13K2OM219`hKTQ&|41l<$a(GUJaKuiVLk
zQbQBF9A$clHGy#vMhH^w-#*$LY(dk1$;YGKb~ify_9hWVPw8m1$Sg2mtw>}t
zt1WbM#kh>y-Ep3Ot4*0B1#Wh<>(y%WIQrY8;k~%cHtZY-iOqj?>29$(ZEbZZRu(MO
z@}yC?W6_w`xMbV?ZcBVl@GfD&X71I)0No?JozrxR%5Ld)=Xv^QB$EJ~d^`O8R1~)J
z({8LmeA>;e-@2Zc8^>Jl&M*Cg^+-qs{ZO~N%4dg?>Ik;?{=|mFZEr{=;dExsr|ZX6
zqLTLRpO+L7abnZigbMucGY)72UXzdcr~UaH4V&afG^=F4d14Z3aiG