}}
28 | * @template State
29 | */
30 | const initRedux = (baseUrl, initialState, reducerCreator) => {
31 | // Create browser history to use in the Redux store
32 | const history = createBrowserHistory({ basename: baseUrl });
33 | const reducer = reducerCreator(history);
34 |
35 | const middleware = [
36 | thunk,
37 | routerMiddleware(history),
38 | ]
39 |
40 | const enhancers = []
41 |
42 | //const isDevelopment = process.env.NODE_ENV === 'development';
43 |
44 | //if (isDevelopment && typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
45 |
46 | if (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
47 | enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__());
48 | }
49 |
50 | const store = createStore(
51 | reducer,
52 | initialState,
53 | compose(
54 | applyMiddleware(...middleware),
55 | ...enhancers
56 | )
57 | );
58 |
59 | return { history, store };
60 | };
61 | export default initRedux;
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build run rm milestone-release release
2 |
3 | IMAGE_NAME=local-niolesk-image
4 | CONTAINER_NAME=local-niolesk
5 |
6 | VERSION_CHECKER=yarn info -R --json | grep "niolesk@workspace:." | jq -M '.children.Version' | sed -e 's/"//g'
7 | APP_VERSION=$(shell $(VERSION_CHECKER))
8 | WORKING_DIR=$(shell pwd)
9 |
10 | PORT=8017
11 |
12 | rm:
13 | docker rm --force $(CONTAINER_NAME) || true
14 |
15 | run: rm
16 | docker run -d --rm=true -e "NIOLESK_KROKI_ENGINE=https://kroki.io/" -p $(PORT):80 --name "$(CONTAINER_NAME)" "$(IMAGE_NAME)"
17 |
18 | build:
19 | docker build --rm --force-rm -t "$(IMAGE_NAME)" .
20 |
21 | release:
22 | bash -c '! [[ `git status --porcelain` ]]' || (echo "You must have committed everything before running a release" && false)
23 | yarn version patch
24 | [ -f "doc/releases/v$$($(VERSION_CHECKER)).md" ] || (echo "You must have a release file for version [v$$($(VERSION_CHECKER))] before creating release" && git reset --hard && false)
25 | git add .
26 | git commit -m "v$$($(VERSION_CHECKER))"
27 | git tag "v$$($(VERSION_CHECKER))"
28 | yarn version patch
29 | git add .
30 | git commit -m "v$$($(VERSION_CHECKER)) : Start new developement"
31 | git push
32 | git push --tags
33 |
34 | milestone-release:
35 | bash -c '! [[ `git status --porcelain` ]]' || (echo "You must have committed everything before running a release" && false)
36 | yarn version minor
37 | [ -f "doc/releases/v$$($(VERSION_CHECKER)).md" ] || (echo "You must have a release file for version [v$$($(VERSION_CHECKER))] before creating release" && git reset --hard && false)
38 | git add .
39 | git commit -m "v$$($(VERSION_CHECKER))"
40 | git tag "v$$($(VERSION_CHECKER))"
41 | yarn version minor
42 | yarn version patch
43 | git add .
44 | git commit -m "v$$($(VERSION_CHECKER)) : Start new developement milestone"
45 | git push
46 | git push --tags
47 |
48 | info:
49 | @echo $(APP_VERSION)
50 |
--------------------------------------------------------------------------------
/src/kroki/coder.test.js:
--------------------------------------------------------------------------------
1 | import { decode, encode } from "./coder"
2 |
3 | const exampleSets = [
4 | {
5 | name: 'Basic',
6 | text: '"a" -> "b"',
7 | base64: 'eNpTSlRS0LVTUEpSAgAKxwH3',
8 | },
9 | {
10 | name: 'With some non ascii char',
11 | text: '"a" -> "é"',
12 | base64: 'eNpTSlRS0LVTUDq8UgkADxEDAQ==',
13 | },
14 | {
15 | name: 'With a CR at the end',
16 | text: '"a" -> "b"\n',
17 | base64: 'eNpTSlRS0LVTUEpS4gIADMgCAQ==',
18 | },
19 | {
20 | name: 'With a CR in the middle',
21 | text: '"a" -> "b"\n"b" -> "c"',
22 | base64: 'eNpTSlRS0LVTUEpS4gJiMDNZCQArmgP5',
23 | },
24 | {
25 | name: 'With a CR in the middle and at the end',
26 | text: '"a" -> "b"\n"b" -> "c"\n',
27 | base64: 'eNpTSlRS0LVTUEpS4gJiMDNZiQsAL50EAw==',
28 | },
29 | {
30 | name: 'With some non ascii char, a CR in the middle and at the end',
31 | text: '"a" -> "é"\n"é" -> "c"\n',
32 | base64: 'eNpTSlRS0LVTUDq8UokLRIA5yUpcAE6zBhc=',
33 | },
34 | {
35 | name: 'With an emoji',
36 | text: '"a" -> "🚆"\n"🚆" -> "c"',
37 | base64: 'eNpTSlRS0LVTUPowf1abEheEAgskKwEAeTEIkw==',
38 | },
39 | ]
40 |
41 | describe('encode', ()=>{
42 | const testExample = ({name, text, base64}) => {
43 | it(`should encode correctly for the test [${name}]`, () => {
44 | expect(encode(text)).toBe(base64);
45 | })
46 | }
47 | exampleSets.forEach((testSet) => testExample(testSet));
48 | })
49 |
50 | describe('decode', ()=>{
51 | const testExample = ({name, text, base64}) => {
52 | it(`should decode correctly for the test [${name}]`, () => {
53 | expect(decode(base64)).toBe(text);
54 | })
55 | }
56 | exampleSets.forEach((testSet) => testExample(testSet));
57 | })
58 |
59 |
--------------------------------------------------------------------------------
/create-example-cache.js:
--------------------------------------------------------------------------------
1 | import data from './src/examples/data'
2 | import { createHash } from 'crypto'
3 | import axios from 'axios'
4 | import util from 'util'
5 | import stream from 'stream'
6 | import fs from 'fs'
7 |
8 | const pipeline = util.promisify(stream.pipeline)
9 | const fileExists = (path) => new Promise((resolve, reject) => {
10 | fs.access(path, fs.F_OK, (err) => {
11 | if (err) {
12 | resolve(false);
13 | } else {
14 | resolve(true);
15 | }
16 | })
17 | })
18 |
19 | const writeToFile = (path, content) => {
20 | return new Promise((resolve, reject) => {
21 | const file = fs.createWriteStream(path);
22 | file.write(content);
23 | file.end();
24 | file.on("finish", () => { resolve(true); });
25 | file.on("error", reject);
26 | });
27 | }
28 |
29 | const md5 = (s) => createHash('md5').update(s).digest('hex');
30 | const engine = 'https://kroki.io'
31 | const dirname = './public/cache'
32 |
33 | const createCache = async (example) => {
34 | const ext = 'svg'
35 | const radical = [example.diagramType, 'svg', example.example].join('/')
36 | const url = `${engine}/${radical}`
37 | const sum = md5(radical)
38 |
39 | const response = await axios.get(url, { responseType: 'stream' })
40 | const filename = `${sum}.${ext}`
41 | const fullFilename = `${dirname}/${filename}`
42 | if (! await fileExists(fullFilename)) {
43 | console.log(`${sum} : ${example.title} - ${example.description}`)
44 | await pipeline(response.data, fs.createWriteStream(fullFilename))
45 | }
46 | return filename
47 | }
48 |
49 | const main = async () => {
50 | if (! await fileExists(dirname)) {
51 | await fs.promises.mkdir(dirname)
52 | }
53 | const cache = await Promise.all(data.map(createCache))
54 | await writeToFile('./public/cache.js', `window.cache = ${JSON.stringify(cache.sort())};`)
55 | }
56 |
57 | main()
--------------------------------------------------------------------------------
/src/views/CopyField/CopyField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classnames from 'classnames'
3 | import PropTypes from 'prop-types';
4 |
5 | import './CopyField.css'
6 | import { Button, TextArea } from 'semantic-ui-react';
7 |
8 | const CopyField = ({ text, onCopy, onCopyHover, isCopyHover, isCopied, scope, isMultiline }) => {
9 | if (!onCopy) {
10 | onCopy = (scope, text) => { };
11 | }
12 | if (!onCopyHover) {
13 | onCopyHover = (scope, isHover) => { };
14 | }
15 |
16 | return
17 |
27 |
38 | }
39 |
40 | CopyField.propTypes = {
41 | text: PropTypes.string.isRequired,
42 | onCopy: PropTypes.func.isRequired,
43 | onCopyHover: PropTypes.func.isRequired,
44 | isCopyHover: PropTypes.bool.isRequired,
45 | isCopied: PropTypes.bool.isRequired,
46 | scope: PropTypes.string.isRequired,
47 | isMultiline: PropTypes.bool.isRequired,
48 | };
49 |
50 | CopyField.defaultProps = {
51 | text: '',
52 | isCopyHover: false,
53 | isCopied: false,
54 | scope: 'image',
55 | isMultiline: false,
56 | };
57 |
58 | export default CopyField;
59 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
20 |
21 |
30 | Niolesk
31 |
32 |
33 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/views/Window/Window.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Window from './Window';
3 | import { getComponenent } from '../storybook/stories';
4 | import ExampleCards from '../ExampleCards/ExampleCards'
5 | import ExampleDetail from '../ExampleDetail/ExampleDetail'
6 | import { Button } from 'semantic-ui-react';
7 | import exampleData from '../../examples/data';
8 | import { createKrokiUrl } from '../../kroki/utils';
9 |
10 | export default {
11 | title: 'Components/Window',
12 | component: Window,
13 | };
14 |
15 | const Template = (args) => ;
16 |
17 | const cards = exampleData.map((example)=>({
18 | diagType: example.title,
19 | description: example.description,
20 | diagUrl: createKrokiUrl('https://kroki.io/', example.diagramType, 'svg', example.example),
21 | onView: () => {},
22 | onImport: () => {},
23 | }))
24 |
25 |
26 | const defaultArgs = {
27 | title: "This is a title",
28 | children: This is a content
,
29 | open: true,
30 | isContentCentered: true,
31 | actions: null,
32 | };
33 |
34 | export const Default = getComponenent(Template, { ...defaultArgs });
35 | export const WithExampleCards = getComponenent(Template, { ...defaultArgs, children: });
36 | export const WithExampleCardsNotCentered = getComponenent(Template, { ...defaultArgs, children: , isContentCentered: false });
37 | export const WithExampleCardsWithAction = getComponenent(Template, { ...defaultArgs, children: , actions: });
38 | export const WithExampleCardsWithActions = getComponenent(Template, { ...defaultArgs, children: , actions: [, ] });
39 | export const WithExampleDetail = getComponenent(Template, { ...defaultArgs, children: });
40 | export const WithExampleDetailWithImportButton = getComponenent(Template, { ...defaultArgs, children: , actions: });
41 |
--------------------------------------------------------------------------------
/src/views/Render/Render.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"
3 |
4 | import './Render.css'
5 |
6 | const Render = ({ diagramUrl, diagramEditUrl, diagramError, onDiagramError, height, width, onEditSizeChanged, shouldRedraw }) => {
7 | const editRef = useRef(null)
8 |
9 | useEffect(() => {
10 | if (!editRef.current) {
11 | return
12 | }
13 | const resizeObserver = new ResizeObserver(() => {
14 | if (onEditSizeChanged) {
15 | onEditSizeChanged(editRef.current.clientWidth, editRef.current.clientHeight)
16 | }
17 | })
18 | resizeObserver.observe(editRef.current)
19 | return () => resizeObserver.disconnect()
20 | }, [onEditSizeChanged])
21 |
22 | return
23 |
24 | {
25 | diagramError ?
26 |
:
27 |
28 | {(utils) => {
29 | if (shouldRedraw) {
30 | utils.resetTransform()
31 | }
32 | return
33 |
34 |

{ onDiagramError(diagramUrl) }} style={{ maxWidth: width, maxHeight: height, }} />
35 |
36 |
37 | }}
38 |
39 | }
40 |
41 |
Edit this diagram.
42 |
43 | }
44 |
45 | export default Render;
--------------------------------------------------------------------------------
/src/views/Editor/Editor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MonacoEditor from '@uiw/react-monacoeditor';
3 | import PropTypes from 'prop-types';
4 | import './Editor.css'
5 | import 'monaco-editor/esm/vs/language/json/json.worker'
6 |
7 | class Editor extends React.Component {
8 | get shouldUpdate() {
9 | return this._shouldUpdate === undefined ? true : this._shouldUpdate
10 | }
11 |
12 | set shouldUpdate(value) {
13 | this._shouldUpdate = value;
14 | }
15 |
16 | shouldComponentUpdate(nextProps) {
17 | if (this._editor && this._editor.editor) {
18 | const editorText = this._editor.editor.getValue();
19 | const nextPropsText = nextProps.text;
20 |
21 | if (nextPropsText === editorText) {
22 | if (nextProps.height !== this.props.height) {
23 | this.shouldUpdate = true;
24 | } else {
25 | this.shouldUpdate = false;
26 | }
27 | } else {
28 | this.shouldUpdate = true;
29 | }
30 | return this.shouldUpdate;
31 | } else {
32 | return true;
33 | }
34 | }
35 |
36 | render() {
37 | const { text, language, onTextChanged, height } = this.props;
38 | const { shouldUpdate } = this;
39 |
40 | return
41 | this._editor = ref}
44 | language={language || "plaintext"}
45 |
46 | onChange={(text) => onTextChanged(text)}
47 | value={shouldUpdate ? text : null}
48 | options={{
49 | theme: 'vs',
50 | automaticLayout: true,
51 | folding: true,
52 | foldingStrategy: 'indentation',
53 | }}
54 | height={`${height}px`}
55 | />
56 |
57 | }
58 | }
59 |
60 | Editor.propTypes = {
61 | text: PropTypes.string,
62 | language: PropTypes.string,
63 | onTextChanged: PropTypes.func.isRequired,
64 | height: PropTypes.number,
65 | };
66 |
67 | export default Editor;
--------------------------------------------------------------------------------
/src/actions/example.test.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_EXAMPLE_INDEX, CHANGE_SEARCH, CLOSE_EXAMPLE, IMPORT_EXAMPLE, NEXT_EXAMPLE, OPEN_EXAMPLES, PREV_EXAMPLE, VIEW_EXAMPLE } from "../constants/example";
2 | import { changeExampleIndex, closeExample, importExample, nextExample, openExamples, prevExample, updateSearch, viewExample } from "./example";
3 |
4 | describe('openExamples', () => {
5 | it(`should dispatch the correct action`, () => {
6 | const result = openExamples();
7 | expect(result).toStrictEqual({ type: OPEN_EXAMPLES })
8 | })
9 | })
10 |
11 | describe('changeExampleIndex', () => {
12 | it(`should dispatch the correct action`, () => {
13 | const result = changeExampleIndex(17);
14 | expect(result).toStrictEqual({ type: CHANGE_EXAMPLE_INDEX, exampleIndex: 17 })
15 | })
16 | })
17 |
18 | describe('importExample', () => {
19 | it(`should dispatch the correct action`, () => {
20 | const result = importExample('A -> B; B -> C;', 'grutDiagram');
21 | expect(result).toStrictEqual({ type: IMPORT_EXAMPLE, diagramText: 'A -> B; B -> C;', diagramType: 'grutDiagram' })
22 | })
23 | })
24 |
25 | describe('viewExample', () => {
26 | it(`should dispatch the correct action`, () => {
27 | const result = viewExample(17);
28 | expect(result).toStrictEqual({ type: VIEW_EXAMPLE, exampleIndex: 17 })
29 | })
30 | })
31 |
32 | describe('closeExample', () => {
33 | it(`should dispatch the correct action`, () => {
34 | const result = closeExample();
35 | expect(result).toStrictEqual({ type: CLOSE_EXAMPLE })
36 | })
37 | })
38 |
39 | describe('prevExample', () => {
40 | it(`should dispatch the correct action`, () => {
41 | const result = prevExample();
42 | expect(result).toStrictEqual({ type: PREV_EXAMPLE })
43 | })
44 | })
45 |
46 | describe('nextExample', () => {
47 | it(`should dispatch the correct action`, () => {
48 | const result = nextExample();
49 | expect(result).toStrictEqual({ type: NEXT_EXAMPLE })
50 | })
51 | })
52 |
53 | describe('updateSearch', () => {
54 | it(`should dispatch the correct action`, () => {
55 | const result = updateSearch('vega pyr');
56 | expect(result).toStrictEqual({ type: CHANGE_SEARCH, search: 'vega pyr' });
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/public/config-analytic-providers.js:
--------------------------------------------------------------------------------
1 | window.config_analytic_providers = {
2 | matomo_js: [
3 | {
4 | type: 'js',
5 | content: `
6 | var _paq = window._paq = window._paq || [];
7 | /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
8 | _paq.push(['trackPageView']);
9 | _paq.push(['enableLinkTracking']);
10 | (function() {
11 | var u="{1}";
12 | _paq.push(['setTrackerUrl', u+'matomo.php']);
13 | _paq.push(['setSiteId', '{2}']);
14 | var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
15 | g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
16 | })();
17 | `,
18 | },
19 | ],
20 | matomo_image: [
21 | {
22 | type: 'html',
23 | content: `
24 |
25 |
26 |
27 | `,
28 | },
29 | ],
30 | google_ga4: [
31 | {
32 | type: 'js',
33 | content: 'https://www.googletagmanager.com/gtag/js?id={1}'
34 | },
35 | {
36 | type: 'js',
37 | content: `
38 | window.dataLayer = window.dataLayer || [];
39 | function gtag(){dataLayer.push(arguments);}
40 | gtag('js', new Date());
41 | gtag('config', '{1}');
42 | `,
43 | },
44 | ],
45 | google_tag: [
46 | {
47 | type: 'js',
48 | content: `
49 | (function(w,d,s,l,i){
50 | w[l]=w[l]||[];
51 | w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
52 | var f=d.getElementsByTagName(s)[0],
53 | j=d.createElement(s),
54 | dl=l!='dataLayer'?'&l='+l:'';
55 | j.async=true;
56 | j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
57 | })(window,document,'script','dataLayer','{1}')
58 | `,
59 | },
60 | ]
61 | };
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG SOURCE=local
2 | ARG IMAGE_BUILD=node:16-alpine3.17
3 | ARG NGINXIMAGE=nginx:alpine
4 |
5 | #----------------------------------------
6 |
7 | FROM --platform=${BUILDPLATFORM} ${IMAGE_BUILD} AS builder-base
8 |
9 | RUN \
10 | apk update && \
11 | apk add git
12 |
13 | #----------------------------------------
14 |
15 | FROM builder-base AS builder-git
16 |
17 | ARG REPO=https://github.com/webgiss/niolesk
18 | ARG POINT=main
19 |
20 | RUN \
21 | git clone "${REPO}" /app && \
22 | cd /app && \
23 | git checkout "${POINT}"
24 |
25 | #----------------------------------------
26 |
27 | FROM builder-base AS builder-local
28 |
29 | ARG PUBLIC_URL=/
30 |
31 | ADD . /app
32 |
33 | #----------------------------------------
34 |
35 | FROM builder-${SOURCE} AS builder
36 |
37 | ARG TARGETARCH
38 | ARG TARGETOS
39 | ENV npm_config_target_arch=$TARGETARCH
40 | ENV npm_config_target_platform=$TARGETOS
41 | WORKDIR /app
42 | RUN \
43 | yarn && \
44 | yarn create-example-cache && \
45 | PUBLIC_URL=${PUBLIC_URL} yarn build
46 |
47 | #----------------------------------------
48 |
49 | FROM ${NGINXIMAGE}
50 |
51 | ARG VCS_REF=working-copy
52 | ARG BUILD_DATE=now
53 | ARG VERSION=dev
54 |
55 | LABEL \
56 | org.opencontainers.image.created="${BUILD_DATE}" \
57 | org.opencontainers.image.authors="gissehel" \
58 | org.opencontainers.image.url="https://github.com/webgiss/niolesk" \
59 | org.opencontainers.image.source="https://github.com/webgiss/niolesk" \
60 | org.opencontainers.image.version="${VERSION}" \
61 | org.opencontainers.image.revision="${VCS_REF}" \
62 | org.opencontainers.image.vendor="gissehel" \
63 | org.opencontainers.image.ref.name="ghcr.io/webgiss/niolesk" \
64 | org.opencontainers.image.title="niolesk" \
65 | org.opencontainers.image.description="Image for niolesk" \
66 | org.label-schema.build-date="${BUILD_DATE}" \
67 | org.label-schema.vcs-ref="${VCS_REF}" \
68 | org.label-schema.name="niolesk" \
69 | org.label-schema.version="${VERSION}" \
70 | org.label-schema.vendor="gissehel" \
71 | org.label-schema.vcs-url="https://github.com/webgiss/niolesk" \
72 | org.label-schema.schema-version="1.0" \
73 | maintainer="Gissehel "
74 |
75 | COPY --from=builder /app/docker-res/update-config.sh /docker-entrypoint.d/update-config.sh
76 | COPY --from=builder /app/build/ /usr/share/nginx/html/
77 | COPY --chmod=0666 build/config.js /usr/share/nginx/html/
78 |
--------------------------------------------------------------------------------
/docker-res/update-config.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | [ -z "${NIOLESK_KROKI_ENGINE}" ] && NIOLESK_KROKI_ENGINE="https://kroki.io"
4 |
5 | CONFIG=$(cat /usr/share/nginx/html/config.js)
6 | CONFIG=$(echo "${CONFIG}" | sed -e 's|krokiEngineUrl: '"'"'.*'"'"'|krokiEngineUrl: '"'${NIOLESK_KROKI_ENGINE}'"'|')
7 | [ -n "${NIOLESK_ANALYTICS_CONTENT}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsContent: '"'"'.*'"'"'|analyticsContent: '"'${NIOLESK_ANALYTICS_CONTENT}'"'|')
8 | [ -n "${NIOLESK_ANALYTICS_TYPE}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsType: '"'"'.*'"'"'|analyticsType: '"'${NIOLESK_ANALYTICS_TYPE}'"'|')
9 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_NAME}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderName: '"'"'.*'"'"'|analyticsProviderName: '"'${NIOLESK_ANALYTICS_PROVIDER_NAME}'"'|')
10 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG1}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg1: '"'"'.*'"'"'|analyticsProviderArg1: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG1}'"'|')
11 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG2}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg2: '"'"'.*'"'"'|analyticsProviderArg2: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG2}'"'|')
12 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG3}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg3: '"'"'.*'"'"'|analyticsProviderArg3: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG3}'"'|')
13 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG4}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg4: '"'"'.*'"'"'|analyticsProviderArg4: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG4}'"'|')
14 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG5}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg5: '"'"'.*'"'"'|analyticsProviderArg5: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG5}'"'|')
15 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG6}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg6: '"'"'.*'"'"'|analyticsProviderArg6: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG6}'"'|')
16 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG7}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg7: '"'"'.*'"'"'|analyticsProviderArg7: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG7}'"'|')
17 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG8}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg8: '"'"'.*'"'"'|analyticsProviderArg8: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG8}'"'|')
18 | [ -n "${NIOLESK_ANALYTICS_PROVIDER_ARG9}" ] && CONFIG=$(echo "${CONFIG}" | sed -e 's|analyticsProviderArg9: '"'"'.*'"'"'|analyticsProviderArg9: '"'${NIOLESK_ANALYTICS_PROVIDER_ARG9}'"'|')
19 | echo "${CONFIG}" > /usr/share/nginx/html/config.js
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "niolesk",
3 | "version": "1.7.7",
4 | "private": true,
5 | "homepage": "https://niolesk.top",
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.16.1",
8 | "@testing-library/react": "^12.1.2",
9 | "@testing-library/user-event": "^13.5.0",
10 | "@uiw/react-monacoeditor": "^3.4.6",
11 | "assert": "^2.0.0",
12 | "axios": "^0.24.0",
13 | "classnames": "^2.3.1",
14 | "connected-react-router": "^6.9.1",
15 | "copy-to-clipboard": "^3.3.1",
16 | "crypto-js": "^4.1.1",
17 | "fomantic-ui-css": "^2.8.8",
18 | "history": "4.10.1",
19 | "pako": "^2.0.4",
20 | "prop-types": "^15.8.0",
21 | "react": "^17.0.2",
22 | "react-dom": "^17.0.2",
23 | "react-router": "^5.2.0",
24 | "react-router-dom": "^5.2.0",
25 | "react-zoom-pan-pinch": "^3.0.7",
26 | "redux": "^4.1.2",
27 | "redux-thunk": "^2.4.1",
28 | "semantic-ui-react": "^2.0.4",
29 | "web-vitals": "^2.1.2"
30 | },
31 | "scripts": {
32 | "create-example-cache": "node -r esm create-example-cache.js",
33 | "hygen": "hygen",
34 | "start": "react-scripts start",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test",
37 | "eject": "react-scripts eject",
38 | "test:html": "react-scripts test --reporters=\"jest-html-reporters\" --all",
39 | "predeploy": "yarn build",
40 | "deploy": "gh-pages -d build",
41 | "storybook": "start-storybook -p 6006 -s public",
42 | "build-storybook": "build-storybook -s public"
43 | },
44 | "engines": {
45 | "npm": "please-use-yarn",
46 | "yarn": ">= 1.20.0"
47 | },
48 | "eslintConfig": {
49 | "extends": [
50 | "react-app",
51 | "react-app/jest"
52 | ],
53 | "overrides": [
54 | {
55 | "files": [
56 | "**/*.stories.*"
57 | ],
58 | "rules": {
59 | "import/no-anonymous-default-export": "off"
60 | }
61 | }
62 | ]
63 | },
64 | "browserslist": {
65 | "production": [
66 | ">0.2%",
67 | "not dead",
68 | "not op_mini all"
69 | ],
70 | "development": [
71 | "last 1 chrome version",
72 | "last 1 firefox version",
73 | "last 1 safari version"
74 | ]
75 | },
76 | "resolutions": {
77 | "babel-loader": "8.1.0",
78 | "webpack": "^5.65.0"
79 | },
80 | "devDependencies": {
81 | "@storybook/addon-actions": "^6.4.9",
82 | "@storybook/addon-essentials": "^6.4.9",
83 | "@storybook/addon-links": "^6.4.9",
84 | "@storybook/builder-webpack5": "^6.4.9",
85 | "@storybook/manager-webpack5": "^6.4.9",
86 | "@storybook/node-logger": "^6.4.9",
87 | "@storybook/preset-create-react-app": "^4.0.0",
88 | "@storybook/react": "^6.4.9",
89 | "esm": "^3.2.25",
90 | "gh-pages": "^3.2.3",
91 | "hygen": "^6.1.0",
92 | "jest-html-reporters": "^2.1.6",
93 | "react-git-info": "^2.0.0",
94 | "react-redux": "^7.2.6",
95 | "react-scripts": "^5.0.0"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/views/App/App.stories.js:
--------------------------------------------------------------------------------
1 | import App from './App';
2 | import { getComponenent } from '../storybook/stories'
3 | import { getReduxMockDecorator } from '../storybook/store';
4 | import diagramTypes from "../../kroki/krokiInfo";
5 | import { decode } from "../../kroki/coder";
6 | import exampleData from "../../examples/data";
7 |
8 | export default {
9 | title: 'Pages/App',
10 | component: App,
11 | };
12 |
13 | const defaultDiagramType = 'plantuml';
14 |
15 | const defaultState = {
16 | editor: {
17 | baseUrl: window.location.origin + window.location.pathname,
18 | hash: null,
19 | diagramType: defaultDiagramType,
20 | diagramText: decode(diagramTypes[defaultDiagramType].example),
21 | filetype: 'svg',
22 | diagramTypes,
23 | renderUrl: 'https://kroki.io/',
24 | diagramUrl: 'https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==',
25 | diagramEditUrl: 'https://niolesk.top/#https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==',
26 | scopes: {
27 | 'image': {
28 | isHover: false,
29 | isCopied: false,
30 | },
31 | 'edit': {
32 | isHover: false,
33 | isCopied: false,
34 | },
35 | 'markdown': {
36 | isHover: false,
37 | isCopied: false,
38 | },
39 | 'markdownsource': {
40 | isHover: false,
41 | isCopied: false,
42 | },
43 | },
44 | windowImportUrlOpened: false,
45 | windowImportUrl: '',
46 | diagramError: false,
47 | zenMode: false,
48 | height: null,
49 | width: null,
50 | renderHeight: 680,
51 | editorHeight: 700,
52 | renderWidth: 586,
53 | renderEditHeight: 0,
54 | redrawIndex: 0,
55 | },
56 | example: {
57 | windowExampleCardsOpened: false,
58 | windowExampleDetailsOpened: false,
59 | exampleIndex: 2,
60 | examples: exampleData,
61 | filteredExamples: exampleData,
62 | search: '',
63 | }
64 | };
65 |
66 |
67 | const Template = (args) => {
68 | const { setState, decorator } = getReduxMockDecorator()
69 | let state = defaultState
70 | if (args.windowExampleCardsOpened !== undefined) {
71 | state = { ...state, example: { ...state.example, windowExampleCardsOpened: args.windowExampleCardsOpened } }
72 | }
73 | if (args.windowExampleDetailsOpened !== undefined) {
74 | state = { ...state, example: { ...state.example, windowExampleDetailsOpened: args.windowExampleDetailsOpened } }
75 | }
76 |
77 | setState(state)
78 | return decorator(() => )
79 | };
80 |
81 | const defaultArgs = {
82 | }
83 |
84 | export const Default = getComponenent(Template, { ...defaultArgs })
85 | export const WithWindowExampleCardsOpened = getComponenent(Template, { ...defaultArgs, windowExampleCardsOpened: true })
86 | export const WithWindowExampleDetailsOpened = getComponenent(Template, { ...defaultArgs, windowExampleDetailsOpened: true })
87 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: 'deploy'
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | deploy:
10 | name: 'Deploy'
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: "Checkout"
14 | uses: "actions/checkout@v3"
15 |
16 | - name : "Configure git"
17 | run : |
18 | git config "user.name" "github-actions"
19 | git config "user.email" "public-dev-github-actions-niolesk@gissehel.org"
20 |
21 | - name: "Set environment variables"
22 | run: |
23 | echo "REFNAME=$(echo "${{ github.ref }}" | sed -e 's/.*\///')" >> "${GITHUB_ENV}"
24 |
25 | - name: "Workaround in case of release documentation doesn't exist"
26 | run: |
27 | filename="doc/releases/${{ env.REFNAME }}.md"
28 | [ -f "${filename}" ] || touch "${filename}"
29 |
30 | - name: "Setup node"
31 | uses: "actions/setup-node@v3"
32 | with:
33 | node-version: 16
34 |
35 | - name: "Build"
36 | run: |
37 | yarn
38 | yarn create-example-cache
39 | yarn build
40 | yarn build-storybook
41 | yarn test:html
42 |
43 | - name: "Create Niolesk assets"
44 | run: |
45 | ref="${{ github.ref }}"
46 |
47 | site_name="niolesk-site-${ref#refs/tags/v*}"
48 | storybook_name="niolesk-storybook-${ref#refs/tags/v*}"
49 | test_name="niolesk-test-${ref#refs/tags/v*}"
50 |
51 | mv "build" "${site_name}"
52 | zip -r "${site_name}.zip" "${site_name}"
53 | tar cvzf "${site_name}.tar.gz" "${site_name}"
54 | mv "${site_name}" build
55 |
56 | mv "storybook-static" "${storybook_name}"
57 | zip -r "${storybook_name}.zip" "${storybook_name}"
58 | tar cvzf "${storybook_name}.tar.gz" "${storybook_name}"
59 | mv "${storybook_name}" storybook
60 |
61 | mv "jest_html_reporters.html" "${test_name}.html"
62 | cp "${test_name}.html" "test.html"
63 |
64 | - name: "Move storybook to build dir"
65 | run: |
66 | echo "niolesk.top" > build/CNAME
67 | mv storybook build/storybook
68 | mkdir -p build/test
69 | mv test.html build/test/index.html
70 | touch build/.nojekyll
71 |
72 | - name: "Deploy to GitHub Pages"
73 | if: success()
74 | uses: crazy-max/ghaction-github-pages@v2
75 | with:
76 | target_branch: gh-pages
77 | build_dir: build
78 | env:
79 | GITHUB_TOKEN: ${{ secrets.DEPLOY_PAT }}
80 |
81 | - name: "Create release"
82 | if: success()
83 | uses: "softprops/action-gh-release@v1"
84 | with:
85 | body:
86 | body_path: doc/releases/${{ env.REFNAME }}.md
87 | files: |
88 | niolesk-*.zip
89 | niolesk-*.tar.gz
90 | niolesk-*.html
91 | draft: false
92 | prerelease: false
93 |
--------------------------------------------------------------------------------
/src/views/ExampleDetail/ExampleDetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, Header, List, Segment } from 'semantic-ui-react';
3 | import PropTypes from 'prop-types';
4 |
5 | import './ExampleDetail.css'
6 |
7 | const ExampleDetail = ({ diagramText, diagramType, description, diagUrl, items, itemIndex, onSelectItem, doc }) => {
8 | const iconName = 'pie graph'
9 | if (!onSelectItem) {
10 | onSelectItem = (index) => { };
11 | }
12 | // console.log('items', items)
13 | return
14 |
15 |
16 |
17 | {items.map((item, index) => onSelectItem(index)}>
18 |
19 |
20 | {item[0]}
21 | {item[1]}
22 |
23 | )}
24 |
25 |
26 |
27 |
28 |
29 | {
30 | doc ? Documentation : {doc} : null
31 | }
32 |
33 |
34 | {diagramText}
35 |
36 | {
37 | doc ? Documentation : {doc} : null
38 | }
39 |
40 |
41 |
42 | }
43 |
44 | ExampleDetail.propTypes = {
45 | diagramText: PropTypes.string.isRequired,
46 | diagramType: PropTypes.string.isRequired,
47 | description: PropTypes.string.isRequired,
48 | diagUrl: PropTypes.string.isRequired,
49 | items: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
50 | itemIndex: PropTypes.number.isRequired,
51 | onSelectItem: PropTypes.func.isRequired,
52 | doc: PropTypes.string,
53 | };
54 |
55 | ExampleDetail.defaultProps = {
56 | diagramType: 'Diagram Type',
57 | description: 'Description',
58 | diagUrl: 'https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==',
59 | diagramText: [
60 | 'blockdiag {',
61 | ' Kroki -> generates -> "Block diagrams";',
62 | ' Kroki -> is -> "very easy!";',
63 | '',
64 | ' Kroki [color = "greenyellow"];',
65 | ' "Block diagrams" [color = "pink"];',
66 | ' "very easy!" [color = "orange"];',
67 | '}'
68 | ].join('\n'),
69 | items: [['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description'], ['Poide', 'Description']],
70 | itemIndex: 1,
71 | };
72 |
73 | export default ExampleDetail;
74 |
--------------------------------------------------------------------------------
/src/reducers/example.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from "./utils/createReducer";
2 | import { CHANGE_EXAMPLE_INDEX, CHANGE_SEARCH, CLOSE_EXAMPLE, IMPORT_EXAMPLE, NEXT_EXAMPLE, OPEN_EXAMPLES, PREV_EXAMPLE, VIEW_EXAMPLE } from '../constants/example'
3 | import { KEY_PRESSED } from '../constants/editor'
4 | import exampleData from '../examples/data';
5 | import { getExampleUrl } from '../examples/usecache'
6 |
7 | const mathMod = (v, m) => ((v % m) + m) % m;
8 |
9 | const examples = exampleData.map((exampleItem, id) => ({
10 | id,
11 | ...exampleItem,
12 | searchField: `${exampleItem.title} ${exampleItem.description} ${exampleItem.keywords ? exampleItem.keywords.join(' ') : ''}`.toLowerCase(),
13 | url: getExampleUrl(exampleItem),
14 | }));
15 |
16 | export const initialState = {
17 | windowExampleCardsOpened: false,
18 | windowExampleDetailsOpened: false,
19 | exampleIndex: 0,
20 | examples,
21 | filteredExamples: examples,
22 | search: '',
23 | };
24 |
25 | const filterExamples = (examples, search) => {
26 | const parts = search.split(' ').filter((part) => part.length > 0).map((part) => part.toLowerCase())
27 | return examples.filter((example) => parts.map((part) => example.searchField.indexOf(part) >= 0).reduce((x, y) => x && y, true))
28 | }
29 |
30 | export default createReducer({
31 | [OPEN_EXAMPLES]: (state, action) => {
32 | if (!state.windowExampleCardsOpened) {
33 | state = { ...state, windowExampleCardsOpened: true, search: '', filteredExamples: state.examples }
34 | }
35 | return state;
36 | },
37 | [VIEW_EXAMPLE]: (state, action) => {
38 | const { exampleIndex } = action
39 | state = { ...state, exampleIndex, windowExampleCardsOpened: false, windowExampleDetailsOpened: true }
40 | return state;
41 | },
42 | [CHANGE_EXAMPLE_INDEX]: (state, action) => {
43 | const { exampleIndex } = action
44 | if (state.exampleIndex !== exampleIndex) {
45 | state = { ...state, exampleIndex }
46 | }
47 | return state;
48 | },
49 | [IMPORT_EXAMPLE]: (state, action) => {
50 | if (state.windowExampleCardsOpened || state.windowExampleDetailsOpened) {
51 | state = { ...state, windowExampleCardsOpened: false, windowExampleDetailsOpened: false, search: '', filteredExamples: state.examples }
52 | }
53 | return state;
54 | },
55 | [CLOSE_EXAMPLE]: (state, action) => {
56 | if (state.windowExampleCardsOpened || state.windowExampleDetailsOpened) {
57 | state = { ...state, windowExampleCardsOpened: false, windowExampleDetailsOpened: false, search: '', filteredExamples: state.examples }
58 | }
59 | return state;
60 | },
61 | [PREV_EXAMPLE]: (state, action) => {
62 | const { exampleIndex, examples } = state;
63 | state = { ...state, exampleIndex: mathMod(exampleIndex - 1, examples.length) }
64 | return state;
65 | },
66 | [NEXT_EXAMPLE]: (state, action) => {
67 | const { exampleIndex, examples } = state;
68 | state = { ...state, exampleIndex: mathMod(exampleIndex + 1, examples.length) }
69 | return state;
70 | },
71 | [CHANGE_SEARCH]: (state, action) => {
72 | const { search } = action;
73 | if (search !== state.search) {
74 | state = { ...state, search, filteredExamples: filterExamples(state.examples, search) }
75 | }
76 | return state;
77 | },
78 | [KEY_PRESSED]: (state, action) => {
79 | const { key, ctrlKey, shiftKey, altKey, metaKey } = action
80 | if (key === 'x' && (!ctrlKey) && (!shiftKey) && (altKey) && (!metaKey)) {
81 | if (!state.windowExampleCardsOpened) {
82 | state = { ...state, windowExampleCardsOpened: true, search: '', filteredExamples: state.examples }
83 | }
84 | }
85 | return state;
86 | },
87 | }, initialState);
88 |
--------------------------------------------------------------------------------
/compilation.md:
--------------------------------------------------------------------------------
1 | # Compilation of Niolesk
2 |
3 | Niolesk currently is just a static page, thus doesn't require anything else than a webserver serving static pages (like github pages).
4 |
5 | ## Compilation using yarn
6 |
7 | Requirements:
8 | - nodejs 16
9 | - yarn
10 | - git
11 |
12 | Note: `node` version 17 has issues when compiling the site, so you should use node LTS which is `node` 16
13 |
14 | Get the sources:
15 |
16 | ```
17 | $ git clone https://github.com/webgiss/niolesk
18 | ```
19 |
20 | Then initialise your working copy
21 |
22 | ```
23 | $ yarn
24 | yarn install v1.22.10
25 | [1/5] Validating package.json...
26 | [2/5] Resolving packages...
27 |
28 | [...]
29 |
30 | [5/5] Building fresh packages...
31 | Done in 30.40s.
32 | $
33 | ```
34 |
35 | Finally build Niolesk
36 |
37 | ```
38 | $ yarn build
39 | yarn run v1.22.10
40 | $ react-scripts build
41 | Creating an optimized production build...
42 |
43 | [...]
44 |
45 | Done in 154.64s.
46 | $
47 | ```
48 |
49 | Niolesk will be found in `build/` (and should be hosted at the root of the website)
50 |
51 | ### Compilation to use in another path than /
52 |
53 | You can compile niolesk to host in another path than the default `/`. You should then replace
54 |
55 | ```
56 | $ yarn build
57 | ```
58 |
59 | With:
60 |
61 | ```
62 | $ PUBLIC_URL=/niolesk/ yarn build
63 | ```
64 |
65 | if you want to host the site at `/niolesk/`.
66 |
67 |
68 | ## Compilation using docker
69 |
70 | Requirement:
71 | - Docker
72 |
73 | ### Basic usage
74 |
75 | First Build or pull the image
76 |
77 | ```
78 | $ docker pull ghcr.io/webgiss/niolesk
79 | ```
80 |
81 | The copy the files
82 |
83 | ```
84 | docker run --rm -v $(pwd)/niolesk:/out --entrypoint "/bin/cp" ghcr.io/webgiss/niolesk -r /usr/share/nginx/html /out
85 | ```
86 |
87 | The files be in folder `niolesk/html/`
88 |
89 | ### Pulling an image
90 |
91 | latest version pushed to git main branch has image tagged with "`:main`".
92 |
93 | latest official version pushed to git main branch has image tagged with "`:latest`".
94 |
95 | All version tagged can be retrieved using the tag name.
96 |
97 | You can use other tags, see https://github.com/webgiss/niolesk/pkgs/container/niolesk for a list of tags you can use.
98 |
99 | #### Examples
100 |
101 | ##### Latest release
102 |
103 | ```
104 | docker pull ghcr.io/webgiss/niolesk:latest
105 | ```
106 |
107 | ##### Latest commit in branch main
108 |
109 | ```
110 | docker pull ghcr.io/webgiss/niolesk:main
111 | ```
112 |
113 | ##### Version 1.2.0
114 |
115 | ```
116 | docker pull ghcr.io/webgiss/niolesk:v1.2.0
117 | ```
118 |
119 | ### Building an image
120 |
121 | By default, if you build an image, it will use the current folder to build.
122 |
123 | ```
124 | docker build --rm --force-rm -t local_niolesk_image .
125 | ```
126 |
127 | You can specify some build args to change the behavior
128 |
129 | ```
130 | docker build --rm --force-rm -t local_niolesk_image:git-7438794 --build-arg SOURCE=git --build-arg POINT=7438794 .
131 | ```
132 |
133 | You can also specify the `PUBLIC_URL` for deployment using `--build-arg` (default is `/`):
134 |
135 | ```
136 | docker build --rm --force-rm -t local_niolesk_image --build-arg PUBLIC_URL=/niolesk/ .
137 | ```
138 |
139 | ## Download the site directly from github
140 |
141 | With each release, you can find the html/css/js pages to use for static hosting (on the root of a vhost site) of niolesk in the file `niolesk-site-x.x.x.tar.gz`. A zip version is also available for who are cluless about tar.gz files.
142 |
143 | You can also find with each release the storybook associated with a version under the name `niolesk-storybook-x.x.x.tar.gz`. A zip version is also provided.
144 |
145 | # Configuration of Niolesk
146 |
147 | You can configure the default kroki engine used by niolesk by editing the `config.js` file.
148 |
149 | ```
150 | window.config = {
151 | krokiEngineUrl: 'https://kroki.io/',
152 | };
153 | ```
154 |
155 | If it's not obvious, replace `https://kroki.io/` by the url of the kroki instance you intend to use (for example `https://kroki.example.com/`).
--------------------------------------------------------------------------------
/src/actions/editor.js:
--------------------------------------------------------------------------------
1 | import { COPY_TEXT, TEXT_COPIED, COPY_BUTTON_HOVERED, RENDERURL_CHANGED, DIAGRAM_CHANGED, DIAGRAM_TYPE_CHANGED, DIAGRAM_CHANGED_UPDATE, IMPORT_URL, CLOSE_IMPORT_URL, OPEN_IMPORT_URL, UPDATE_IMPORT_URL, DIAGRAM_HAS_ERROR, ZEN_MODE_CHANGED, WINDOW_RESIZED, KEY_PRESSED, RENDER_EDIT_SIZE_CHANGED } from "../constants/editor";
2 | import delay from "./utils/delay";
3 | import copy from 'copy-to-clipboard';
4 |
5 |
6 | /**
7 | * Copy the text associated to the scope
8 | *
9 | * @param {string} scope
10 | * @param {string} text
11 | */
12 | export const copyText = (scope, text) => async (dispatch, getState) => {
13 | dispatch({ type: COPY_TEXT, scope, text });
14 | copy(text);
15 | dispatch({ type: TEXT_COPIED, scope, isCopied: true });
16 | await delay(1000);
17 | dispatch({ type: TEXT_COPIED, scope, isCopied: false });
18 | };
19 |
20 | /**
21 | * Indicate that the Copy button from the scope is hovered or not.
22 | *
23 | * @param {string} scope
24 | * @param {boolean} isHover
25 | */
26 | export const copyButtonHovered = (scope, isHover) => ({ type: COPY_BUTTON_HOVERED, scope, isHover });
27 |
28 | /**
29 | * Called when the renderUrl has changed.
30 | *
31 | * @param {string} renderUrl
32 | * @returns
33 | */
34 | export const renderUrlChanged = (renderUrl) => ({ type: RENDERURL_CHANGED, renderUrl })
35 |
36 | let lastChange = null;
37 |
38 | /**
39 | * Called when the diagramText has changed.
40 | *
41 | * @param {string} diagramText
42 | * @returns
43 | */
44 | export const diagramChanged = (diagramText) => async (dispatch, getState) => {
45 | dispatch({ type: DIAGRAM_CHANGED, diagramText });
46 | const currentChange = (new Date()).getTime();
47 | lastChange = currentChange;
48 | await delay(750);
49 | if (lastChange === currentChange) {
50 | dispatch({ type: DIAGRAM_CHANGED_UPDATE });
51 | }
52 | }
53 |
54 | /**
55 | * Called when diagramType changed
56 | *
57 | * @param {string} diagramType
58 | * @returns
59 | */
60 | export const diagramTypeChanged = (diagramType) => ({ type: DIAGRAM_TYPE_CHANGED, diagramType });
61 |
62 | /**
63 | * Called when a new diagram URL has been imported
64 | * @param {string} url The diagram url to import
65 | * @returns
66 | */
67 | export const importUrl = (url) => ({ type: IMPORT_URL, url });
68 |
69 | /**
70 | * Called when the new digram url window is closed without importing new URL
71 | * @returns
72 | */
73 | export const closeImportUrl = () => ({ type: CLOSE_IMPORT_URL });
74 |
75 | /**
76 | * Called when the new digram url window should be shown
77 | * @returns
78 | */
79 | export const openImportUrl = () => ({ type: OPEN_IMPORT_URL });
80 |
81 | /**
82 | * Called when the new digram url window update the url
83 | * @returns
84 | */
85 | export const updateUrl = (url) => ({ type: UPDATE_IMPORT_URL, url })
86 |
87 | /**
88 | * Called when the digram url resolve an error
89 | * @returns
90 | */
91 | export const diagramHasError = (url) => ({ type: DIAGRAM_HAS_ERROR, url })
92 |
93 | /**
94 | * Change zen Mode
95 | * @param {boolean} zenMode The change mode to set
96 | * @returns
97 | */
98 | export const changeZenMode = (zenMode) => ({ type: ZEN_MODE_CHANGED, zenMode })
99 |
100 | /**
101 | * Called when a key is pressed
102 | * @param {Object} obj
103 | * @param {string} obj.code
104 | * @param {string} obj.key
105 | * @param {boolean} obj.ctrlKey
106 | * @param {boolean} obj.shiftKey
107 | * @param {boolean} obj.altKey
108 | * @param {boolean} obj.metaKey
109 | * @returns
110 | */
111 | export const keyPressed = ({ code, key, ctrlKey, shiftKey, altKey, metaKey }) => ({ type: KEY_PRESSED, code, key, ctrlKey, shiftKey, altKey, metaKey })
112 |
113 | /**
114 | * Called when the size of the inner window has changed
115 | * @param {number} width The new width of the inner browser window
116 | * @param number} height The new height of the inner browser window
117 | * @returns
118 | */
119 | export const onWindowResized = (width, height) => ({ type: WINDOW_RESIZED, width, height })
120 |
121 | /**
122 | * Called when the width of the render zone has changed
123 | *
124 | * @param {number} renderWidth The width of the render zone
125 | * @returns
126 | */
127 | export const onRenderEditSizeChanged = (renderEditWidth, renderEditHeight) => ({ type: RENDER_EDIT_SIZE_CHANGED, renderEditWidth, renderEditHeight })
--------------------------------------------------------------------------------
/src/views/CopyZone/CopyZone.stories.js:
--------------------------------------------------------------------------------
1 | import CopyZone from './CopyZone';
2 | import { getReduxMockDecorator } from '../storybook/store';
3 | import { getComponenent } from '../storybook/stories'
4 |
5 | const defaultState = {
6 | editor: {
7 | diagramEditUrl: 'poide',
8 | diagramUrl: 'praf',
9 | scopes: {
10 | image: { isHover: false, isCopied: false },
11 | edit: { isHover: false, isCopied: false },
12 | markdown: { isHover: false, isCopied: false },
13 | markdownsource: { isHover: false, isCopied: false },
14 | }
15 | }
16 | }
17 |
18 | const typeString = { type: { name: 'string' } };
19 | const typeBoolean = { type: { name: 'boolean' } }
20 | export default {
21 | title: 'Components/CopyZone',
22 | component: CopyZone,
23 | argTypes: {
24 | diagramUrl: typeString,
25 | diagramEditUrl: typeString,
26 | diagramText: typeString,
27 | isHoverImage: typeBoolean,
28 | isCopiedImage: typeBoolean,
29 | isHoverEdit: typeBoolean,
30 | isCopiedEdit: typeBoolean,
31 | isHoverMarkdown: typeBoolean,
32 | isCopiedMarkdown: typeBoolean,
33 | isHoverMarkdownSource: typeBoolean,
34 | isCopiedMarkdownSource: typeBoolean,
35 | },
36 | };
37 |
38 | const updateStateEditor = (state, args, name) => {
39 | if (args[name]) {
40 | state = {
41 | ...state,
42 | editor: {
43 | ...state.editor,
44 | [name]: args[name],
45 | },
46 | }
47 | }
48 | return state;
49 | }
50 | const updateStateEditorScope = (state, args, scope, name, argName) => {
51 | if (args[argName]) {
52 | state = {
53 | ...state,
54 | editor: {
55 | ...state.editor,
56 | scopes: {
57 | ...state.editor.scopes,
58 | [scope]: {
59 | ...state.editor.scopes[scope],
60 | [name]: args[argName],
61 | },
62 | },
63 | },
64 | }
65 | }
66 | return state;
67 | }
68 |
69 | const Template = (args) => {
70 | let state = defaultState;
71 | state = updateStateEditor(state, args, 'diagramUrl')
72 | state = updateStateEditor(state, args, 'diagramEditUrl')
73 | state = updateStateEditor(state, args, 'diagramText')
74 | state = updateStateEditorScope(state, args, 'image', 'isHover', 'isHoverImage')
75 | state = updateStateEditorScope(state, args, 'image', 'isCopied', 'isCopiedImage')
76 | state = updateStateEditorScope(state, args, 'edit', 'isHover', 'isHoverEdit')
77 | state = updateStateEditorScope(state, args, 'edit', 'isCopied', 'isCopiedEdit')
78 | state = updateStateEditorScope(state, args, 'markdown', 'isHover', 'isHoverMarkdown')
79 | state = updateStateEditorScope(state, args, 'markdown', 'isCopied', 'isCopiedMarkdown')
80 | state = updateStateEditorScope(state, args, 'markdownsource', 'isHover', 'isHoverMarkdownSource')
81 | state = updateStateEditorScope(state, args, 'markdownsource', 'isCopied', 'isCopiedMarkdownSource')
82 |
83 | const { setState, decorator } = getReduxMockDecorator()
84 |
85 | setState(state)
86 | return decorator(CopyZone)
87 | };
88 |
89 | const defaultArgs = {
90 | diagramUrl: 'https://kroki.example.com/dtype/data4879DATA0000==',
91 | diagramEditUrl: 'https://niolesk.example.com/#https://kroki.example.com/dtype/data4879DATA0000==',
92 | diagramText: 'source',
93 | isHoverImage: false,
94 | isCopiedImage: false,
95 | isHoverEdit: false,
96 | isCopiedEdit: false,
97 | isHoverMarkdown: false,
98 | isCopiedMarkdown: false,
99 | isHoverMarkdownSource: false,
100 | isCopiedMarkdownSource: false,
101 | };
102 |
103 | export const Default = getComponenent(Template, {...defaultArgs});
104 | export const WithImageHover = getComponenent(Template, {...defaultArgs, isHoverImage: true});
105 | export const WithImageCopied = getComponenent(Template, {...defaultArgs, isCopiedImage: true});
106 | export const WithImageCopiedHover = getComponenent(Template, {...defaultArgs, isHoverImage: true, isCopiedImage: true});
107 | export const WithEditHover = getComponenent(Template, {...defaultArgs, isHoverEdit: true});
108 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: "build"
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | push:
7 | branches:
8 | - "*"
9 | tags:
10 | - "v*"
11 |
12 | schedule:
13 | - cron: "07 00 * * 4"
14 |
15 | pull_request:
16 | branches:
17 | - "*"
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: "Checkout"
24 | uses: "actions/checkout@v3"
25 |
26 | - name: "Set environment variables"
27 | run: |
28 | # TIPS!! Works as an export replacement, that handles GITHUB_ENV
29 | export_ga() {
30 | for _name in "${@}"
31 | do
32 | local _key="${_name%%=*}"
33 | local _value="${_name#*=}"
34 | [ "${_key}" == "${_name}" ] && _value="${!_name}"
35 | export $_key="${_value}"
36 | echo "${_key}=${_value}" >> "${GITHUB_ENV}"
37 | done
38 | }
39 |
40 | export_ga GITHUB_SHA_SHORT="$(git rev-parse --short HEAD)"
41 | export_ga REPO_NAME="${{ github.event.repository.name }}"
42 | export_ga GH_REGISTRY="ghcr.io"
43 | export_ga GH_USER="${{ github.actor }}"
44 | export_ga GH_OWNER="${{ github.repository_owner }}"
45 |
46 | export_ga BUILD_DATE="$(TZ=Europe/Paris date -Iseconds)"
47 | export_ga REFNAME="$(echo "${{ github.ref }}" | sed -e 's/.*\///')"
48 | export_ga VERSION="$(cat package.json | jq -r '.version')"
49 | export_ga IMAGE_NAME="${GH_REGISTRY}/${GH_OWNER}/${REPO_NAME}"
50 |
51 | export_ga IS_PR="${{ github.event_name == 'pull_request' }}"
52 | export_ga IS_RELEASE="${{ startsWith(github.ref, 'refs/tags/v') }}"
53 |
54 | if [ "${IS_RELEASE}" == "true" ]
55 | then
56 | export_ga VERSION_LABEL="${VERSION}"
57 | export_ga DOCKER_TAGS="${IMAGE_NAME}:${REFNAME},${IMAGE_NAME}:latest"
58 | export_ga DOCKER_UNPRIVILEGED_TAGS="${IMAGE_NAME}:unprivileged-${REFNAME},${IMAGE_NAME}:unprivileged"
59 | else
60 | export_ga VERSION_LABEL="${VERSION}-${GITHUB_SHA_SHORT}"
61 | export_ga DOCKER_TAGS="${IMAGE_NAME}:${GITHUB_SHA_SHORT},${IMAGE_NAME}:${VERSION}-git,${IMAGE_NAME}:${VERSION}-${GITHUB_SHA_SHORT},${IMAGE_NAME}:${REFNAME}-${GITHUB_SHA_SHORT},${IMAGE_NAME}:${REFNAME}"
62 | export_ga DOCKER_UNPRIVILEGED_TAGS="${IMAGE_NAME}:unprivileged-${GITHUB_SHA_SHORT},${IMAGE_NAME}:unprivileged-${VERSION}-git,${IMAGE_NAME}:unprivileged-${VERSION}-${GITHUB_SHA_SHORT},${IMAGE_NAME}:unprivileged-${REFNAME}-${GITHUB_SHA_SHORT},${IMAGE_NAME}:unprivileged-${REFNAME}"
63 | fi
64 |
65 | - name: "Configure git"
66 | run: |
67 | git config "user.name" "github-actions"
68 | git config "user.email" "public-dev-github-actions-niolesk@gissehel.org"
69 |
70 | - name: "Setup node"
71 | uses: "actions/setup-node@v3"
72 | with:
73 | node-version: 16
74 |
75 | - name: "Build application"
76 | run: |
77 | yarn
78 | yarn create-example-cache
79 | yarn build
80 |
81 | - name: "Install cosign"
82 | if: env.IS_PR != 'true'
83 | uses: "sigstore/cosign-installer@v3.0.5"
84 | with:
85 | cosign-release: "v2.0.2"
86 |
87 | - name: "Set up QEMU"
88 | uses: "docker/setup-qemu-action@v2"
89 |
90 | - name: "Setup Docker buildx"
91 | uses: "docker/setup-buildx-action@v2.5.0"
92 |
93 | - name: "Login to github container registry"
94 | uses: "docker/login-action@v2.1.0"
95 | with:
96 | registry: "${{ env.GH_REGISTRY }}"
97 | username: "${{ env.GH_USER }}"
98 | password: "${{ secrets.GITHUB_TOKEN }}"
99 |
100 | - name: "Build and push (unprivileged nginx)"
101 | uses: "docker/build-push-action@v4"
102 | with:
103 | context: "."
104 | platforms: "linux/amd64,linux/arm64,linux/386,linux/arm/v7"
105 | push: ${{ env.IS_PR != 'true' }}
106 | no-cache: true
107 | file: Dockerfile-for-local-build
108 | build-args: |
109 | SOURCE=git
110 | POINT=${{ env.GITHUB_SHA_SHORT }}
111 | VCS_REF=${{ env.GITHUB_SHA_SHORT }}
112 | BUILD_DATE=${{ env.BUILD_DATE }}
113 | VERSION=${{ env.VERSION_LABEL }}
114 | NGINXIMAGE=ghcr.io/nginxinc/nginx-unprivileged:stable-alpine-slim
115 | tags: "${{ env.DOCKER_UNPRIVILEGED_TAGS }}"
116 |
117 | - name: "Build and push (standard nginx)"
118 | uses: "docker/build-push-action@v4"
119 | with:
120 | context: "."
121 | platforms: "linux/amd64,linux/arm64,linux/386,linux/arm/v7"
122 | push: ${{ env.IS_PR != 'true' }}
123 | no-cache: true
124 | file: Dockerfile-for-local-build
125 | build-args: |
126 | SOURCE=git
127 | POINT=${{ env.GITHUB_SHA_SHORT }}
128 | VCS_REF=${{ env.GITHUB_SHA_SHORT }}
129 | BUILD_DATE=${{ env.BUILD_DATE }}
130 | VERSION=${{ env.VERSION_LABEL }}
131 | tags: "${{ env.DOCKER_TAGS }}"
132 |
--------------------------------------------------------------------------------
/src/init/reactRedux.js:
--------------------------------------------------------------------------------
1 | // import exportValues from '../utils/exportValues';
2 | import { initReact } from './react'
3 | import initRedux from './redux'
4 |
5 | /**
6 | * @typedef {import('./redux').Reducer} Reducer
7 | * @template State
8 | */
9 | /**
10 | * @typedef {import('./redux').Store} Store
11 | * @template State
12 | */
13 | /**
14 | * @typedef {import('./redux').AnyAction} AnyAction
15 | */
16 | /**
17 | * @typedef {import('./redux').BrowserHistory} BrowserHistory
18 | */
19 | /**
20 | * @typedef {import('redux').Dispatch} Dispatch
21 | */
22 | /**
23 | * @typedef {import('redux').Unsubscribe} StoreUnsubscribe
24 | */
25 |
26 | /**
27 | * @typedef {Object} SubscribeParams
28 | * @property {Dispatch} dispatch
29 | * @property {Store} store
30 | * @property {BrowserHistory} history
31 | * @template State
32 | */
33 |
34 | /**
35 | * @typedef StateRegistration
36 | * @type {Object}
37 | * @property {string} name
38 | * @property {(state: State) => T} getStateValue
39 | * @property {(currentValue: T, previousValue: T, currentState: State, subscribeParams: SubscribeParams) => void} onNewValue
40 | * @property {T} currentValue
41 | * @template State
42 | * @template T
43 | */
44 |
45 | /**
46 | * @typedef StateChange
47 | * @type {Object}
48 | * @property {string} name
49 | * @property {(state: State) => T} getStateValue
50 | * @property {(currentValue: T, previousValue: T, currentState: State, subscribeParams: SubscribeParams) => void} onNewValue
51 | * @template State
52 | * @template T
53 | */
54 |
55 | /**
56 | * @typedef {StateRegistration[]} StateRegistrations
57 | * @template State
58 | * @template T
59 | */
60 |
61 | /**
62 | * @typedef {Object} Provider
63 | * @property {(state:State)=>State} onInitialState
64 | * @property {(subscribeParams:SubscribeParams)=>void} onStartApplication
65 | * @property {()=>void} onInit
66 | * @property {(state:State)=>void} onNewState
67 | * @property {StateChange[]} stateChangeManager
68 | * @template State
69 | */
70 |
71 | /**
72 | * @typedef {Provider[]} Providers
73 | * @template State
74 | */
75 |
76 | /**
77 | * @type {StateRegistrations}
78 | * @template State
79 | */
80 | const stateRegistrations = [];
81 |
82 | /**
83 | * Register a new State change
84 | *
85 | * @param {string} name
86 | * @param {(state: State) => T} getStateValue
87 | * @param {(currentValue: T, previousValue: T, currentState: State, subscribeParams: SubscribeParams) => void} onNewValue
88 | * @template State
89 | * @template T
90 | */
91 | export const registerStateChange = (name, getStateValue, onNewValue) => {
92 | let stateRegistration = {
93 | name,
94 | getStateValue,
95 | onNewValue,
96 | currentValue: undefined,
97 | };
98 | stateRegistrations.push(stateRegistration);
99 | }
100 |
101 | /**
102 | * Initialize the whole react/redux stack
103 | *
104 | * @param {Providers} providers
105 | * @param {string} baseUrl
106 | * @param {HTMLElement} domNode
107 | * @param {()=>JSX.Element} ReactNode
108 | * @param {(history: BrowserHistory) => Reducer} reducerCreator
109 | * @return {()=>void}
110 | * @template State
111 | */
112 | export const initReactRedux = (providers, baseUrl, domNode, ReactNode, reducerCreator) => {
113 | /** @type {State} */
114 | let initialState = undefined;
115 |
116 | for (let provider of providers) {
117 | if (provider.onInitialState !== undefined) {
118 | initialState = provider.onInitialState(initialState);
119 | }
120 | }
121 |
122 | const { history, store } = initRedux(baseUrl, initialState, reducerCreator);
123 |
124 | let dispatch = store.dispatch;
125 |
126 | /** @type {SubscribeParams} */
127 | let subscribeParams = { dispatch, store, history };
128 |
129 | for (let provider of providers) {
130 | if (provider.onStartApplication !== undefined) {
131 | provider.onStartApplication(subscribeParams);
132 | }
133 | }
134 |
135 | for (let provider of providers) {
136 | if (provider.stateChangeManager !== undefined) {
137 | const stateChanges = provider.stateChangeManager;
138 | for (let stateChange of stateChanges) {
139 | registerStateChange(stateChange.name, stateChange.getStateValue, stateChange.onNewValue);
140 | }
141 | }
142 | }
143 |
144 | let currentState = undefined;
145 |
146 | const unsubscribe = store.subscribe(() => {
147 | currentState = store.getState();
148 |
149 | for (let provider of providers) {
150 | if (provider.onNewState !== undefined) {
151 | provider.onNewState(currentState);
152 | }
153 | }
154 | for (let stateRegistration of stateRegistrations) {
155 | let previousValue = stateRegistration.currentValue;
156 | stateRegistration.currentValue = stateRegistration.getStateValue(currentState);
157 | if (stateRegistration.currentValue !== previousValue) {
158 | stateRegistration.onNewValue(stateRegistration.currentValue, previousValue, currentState, subscribeParams);
159 | }
160 | }
161 | });
162 |
163 | initReact(store, history, domNode, ReactNode);
164 |
165 | for (let provider of providers) {
166 | if (provider.onInit !== undefined) {
167 | provider.onInit();
168 | }
169 | }
170 |
171 | return () => {
172 | unsubscribe();
173 | };
174 | }
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/src/views/App/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types'
3 | import '../fomantic-ui-css/semantic.min.css'
4 | import { Form, Segment } from 'semantic-ui-react';
5 |
6 | import Title from '../Title';
7 | import SubTitle from '../SubTitle';
8 | import Columns from '../Columns';
9 | import Editor from '../Editor';
10 | import Render from '../Render';
11 | import CopyZone from '../CopyZone';
12 | import DiagramType from '../DiagramType';
13 | import RenderUrl from '../RenderUrl';
14 | import ShrinkableButton from '../ShrinkableButton';
15 | import WindowExampleCards from '../WindowExampleCards';
16 | import WindowExampleDetail from '../WindowExampleDetail';
17 | import WindowImportUrl from '../WindowImportUrl';
18 |
19 | import './App.css'
20 | import classNames from 'classnames';
21 |
22 | const App = ({ onExamples, onImportUrl, onSetZenMode, zenMode, onKey, onResize, analytics }) => {
23 | if (!onExamples) {
24 | onExamples = () => { };
25 | }
26 | if (!onImportUrl) {
27 | onImportUrl = () => { };
28 | }
29 |
30 | const hasAnalytics = (analytics ? true : false)
31 | const analyticsJs = hasAnalytics ? (analytics.filter((item) => item.type === 'js')) : []
32 | const analyticsHtml = hasAnalytics ? (analytics.filter((item) => item.type !== 'js')) : []
33 | const hasAnalyticsJs = analyticsJs.length > 0
34 | const hasAnalyticsHtml = analyticsHtml.length > 0
35 |
36 | useEffect(() => {
37 | const handleResize = () => {
38 | const { offsetWidth: width, offsetHeight: height } = document.body;
39 | if (onResize) {
40 | onResize(width, height)
41 | }
42 | }
43 |
44 | const handleKeydown = (e) => {
45 | const { code, key, ctrlKey, shiftKey, altKey, metaKey } = e;
46 | if (onKey) {
47 | onKey({ code, key, ctrlKey, shiftKey, altKey, metaKey })
48 | }
49 | }
50 |
51 | window.addEventListener('resize', handleResize);
52 | window.addEventListener('keydown', handleKeydown);
53 | setTimeout(() => handleResize(), 0);
54 | return () => {
55 | window.removeEventListener('resize', handleResize);
56 | window.removeEventListener('keydown', handleKeydown);
57 | }
58 | });
59 |
60 |
61 | useEffect(() => {
62 | if (hasAnalyticsJs) {
63 | const scripts = analyticsJs.map((analyticsItem)=>{
64 | const script = document.createElement('script')
65 |
66 | script.async = 'true'
67 | const content = analyticsItem.content
68 |
69 | if (content.startsWith('http://') || content.startsWith('https://') || content.startsWith('//')) {
70 | script.setAttribute('src',content)
71 | } else {
72 | script.textContent = content
73 | }
74 |
75 | document.head.appendChild(script)
76 | return script
77 | })
78 |
79 | return () => {
80 | for(let script of scripts) {
81 | document.head.removeChild(script)
82 | }
83 | }
84 | }
85 | return () => { }
86 | })
87 |
88 | return
89 |
90 |
91 |
92 |
93 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | onSetZenMode()} icon='external alternate' text='Zen Mode' textAlt='Zen' />
104 | onImportUrl()} icon='write' text='Import diagram URL' textAlt='URL' />
105 | onExamples()} icon='list alternate outline' text='Examples' textAlt='Ex.' />
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | {
121 | hasAnalyticsHtml ? analyticsHtml.map((item) =>
) : null
122 | }
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | }
132 |
133 | App.propTypes = {
134 | onExamples: PropTypes.func.isRequired,
135 | onImportUrl: PropTypes.func.isRequired,
136 | onSetZenMode: PropTypes.func.isRequired,
137 | onKey: PropTypes.func.isRequired,
138 | onResize: PropTypes.func.isRequired,
139 | };
140 |
141 | export default App;
142 |
--------------------------------------------------------------------------------
/src/reducers/utils/analytics_functions.test.js:
--------------------------------------------------------------------------------
1 | import get_analytic_content from './analytics_functions'
2 |
3 | const default_content = `
4 | analytics_call('https://example.com/analytics', '3;7,15,1,292')
5 | `
6 |
7 | const example_provider_name = 'example'
8 | const example_provider_data = ['https://example.com/analytics', '42']
9 |
10 | const other_provider_name = 'other'
11 | const other_provider_data = ['https://example.com/analytics/142.png']
12 |
13 | const complex_provider_name = 'complex'
14 | const complex_provider_data = ['https://example.com/analytics', '43', 'erk']
15 |
16 | const nonexisting_provider_name = 'nonexisting'
17 | const nonexisting_provider_data = ['https://example.com/analytics', '43', 'erk']
18 |
19 | const nodata_provider_name = 'nodata'
20 | const nodata_provider_data = []
21 |
22 | const multiple_js_provider_name = 'multiple_js'
23 | const multiple_js_provider_data = ['E-B6533HAL42']
24 |
25 | const default_providers = {
26 | 'example': [
27 | {
28 | type: 'js',
29 | content: `
30 | const analytics_url='{1}';
31 | const analytics_id='{2}';
32 | analytics_call(analytics_url, analytics_id)
33 | `,
34 | },
35 | ],
36 | 'other': [
37 | {
38 | type: 'html',
39 | content: `
`,
40 | },
41 | ],
42 | 'complex': [
43 | {
44 | type: 'js',
45 | content: `
46 | const analytics_url='{1}';
47 | const analytics_id='{2}';
48 | analytics_call('{1}', '{2}', '{3}')
49 | `,
50 | },
51 | ],
52 | 'nodata': [
53 | {
54 | type: 'html',
55 | content: `
`,
56 | },
57 | ],
58 | 'multiple_js': [
59 | {
60 | type: 'js',
61 | content: `https://example.com/tag/js?id={1}`,
62 | },
63 | {
64 | type: 'js',
65 | content: `
66 | window.dataExample = window.dataExample || []
67 | const exampleTag = ()=>dataExample.push(arguments)
68 | exampleTag('js',new Date())
69 | exampleTag('config', '{1}')
70 | `,
71 | },
72 | ]
73 | }
74 |
75 | const example_expected_content = [
76 | {
77 | content: `
78 | const analytics_url='https://example.com/analytics';
79 | const analytics_id='42';
80 | analytics_call(analytics_url, analytics_id)
81 | `,
82 | type: 'js',
83 | },
84 | ]
85 |
86 | const other_expected_content = [
87 | {
88 | content: `
`,
89 | type: 'html',
90 | },
91 | ]
92 |
93 | const complex_expected_content = [
94 | {
95 | content: `
96 | const analytics_url='https://example.com/analytics';
97 | const analytics_id='43';
98 | analytics_call('https://example.com/analytics', '43', 'erk')
99 | `,
100 | type: 'js',
101 | },
102 | ]
103 |
104 | const nodata_expected_content = [
105 | {
106 | content: default_providers['nodata'][0]['content'],
107 | type: 'html',
108 | },
109 | ]
110 |
111 | const multiple_js_expected_content = [
112 | {
113 | content: 'https://example.com/tag/js?id=E-B6533HAL42',
114 | type: 'js',
115 | },
116 | {
117 | content: `
118 | window.dataExample = window.dataExample || []
119 | const exampleTag = ()=>dataExample.push(arguments)
120 | exampleTag('js',new Date())
121 | exampleTag('config', 'E-B6533HAL42')
122 | `,
123 | type: 'js',
124 | },
125 | ]
126 |
127 | describe('get_analytic_content', () => {
128 | it('should return the content if provided a non null non empty string', () => {
129 | const content = default_content
130 | const type = 'html'
131 | const provider_name = example_provider_name
132 | const provider_data = example_provider_data
133 | const providers = default_providers
134 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
135 | expect(result).toStrictEqual([{ content: default_content, type: 'html' }]);
136 | })
137 |
138 | it('should return null if provided an empty content, and a provider name that doesnt exists in providers', () => {
139 | const content = ''
140 | const type = ''
141 | const provider_name = nonexisting_provider_name
142 | const provider_data = nonexisting_provider_data
143 | const providers = default_providers
144 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
145 | expect(result).toBeNull();
146 | })
147 |
148 | it('should return null if provided an empty content, and a provider name that doesnt exists in providers even if type is provided not empty', () => {
149 | const content = ''
150 | const type = 'js'
151 | const provider_name = nonexisting_provider_name
152 | const provider_data = nonexisting_provider_data
153 | const providers = default_providers
154 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
155 | expect(result).toBeNull();
156 | })
157 |
158 | it('should return null if provided an empty content, and a provider name that doesnt exists in providers even if type is provided not empty (bis)', () => {
159 | const content = ''
160 | const type = 'html'
161 | const provider_name = nonexisting_provider_name
162 | const provider_data = nonexisting_provider_data
163 | const providers = default_providers
164 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
165 | expect(result).toBeNull();
166 | })
167 |
168 | it('should return data for the correct provider if provided an empty content and a provider that need no data', () => {
169 | const content = ''
170 | const type = ''
171 | const provider_name = nodata_provider_name
172 | const provider_data = nodata_provider_data
173 | const providers = default_providers
174 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
175 | expect(result).toStrictEqual(nodata_expected_content);
176 | })
177 |
178 | it('should return the formated pattern "example" if provided the "example" provider and data', () => {
179 | const content = ''
180 | const type = 'html'
181 | const provider_name = example_provider_name
182 | const provider_data = example_provider_data
183 | const providers = default_providers
184 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
185 | expect(result).toStrictEqual(example_expected_content);
186 | })
187 |
188 | it('should return the formated pattern "example" if provided the "example" provider and data even if the content is null and not empty', () => {
189 | const content = ''
190 | const type = 'html'
191 | const provider_name = example_provider_name
192 | const provider_data = example_provider_data
193 | const providers = default_providers
194 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
195 | expect(result).toStrictEqual(example_expected_content);
196 | })
197 |
198 | it('should return the formated pattern "other" if provided the "other" provider and data', () => {
199 | const content = ''
200 | const type = 'html'
201 | const provider_name = other_provider_name
202 | const provider_data = other_provider_data
203 | const providers = default_providers
204 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
205 | expect(result).toStrictEqual(other_expected_content);
206 | })
207 |
208 | it('should return the formated pattern "complex" if provided the "complex" provider and data', () => {
209 | const content = ''
210 | const type = 'html'
211 | const provider_name = complex_provider_name
212 | const provider_data = complex_provider_data
213 | const providers = default_providers
214 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
215 | expect(result).toStrictEqual(complex_expected_content);
216 | })
217 |
218 | it('should return the formated pattern "multiple_js" if provided the "multiple_js" provider and data', () => {
219 | const content = ''
220 | const type = 'html'
221 | const provider_name = multiple_js_provider_name
222 | const provider_data = multiple_js_provider_data
223 | const providers = default_providers
224 | const result = get_analytic_content(content, type, provider_name, provider_data, providers)
225 | expect(result).toStrictEqual(multiple_js_expected_content);
226 | })
227 | })
--------------------------------------------------------------------------------
/src/actions/editor.test.js:
--------------------------------------------------------------------------------
1 | import { COPY_BUTTON_HOVERED, COPY_TEXT, DIAGRAM_CHANGED, DIAGRAM_CHANGED_UPDATE, DIAGRAM_HAS_ERROR, DIAGRAM_TYPE_CHANGED, IMPORT_URL, KEY_PRESSED, OPEN_IMPORT_URL, RENDERURL_CHANGED, TEXT_COPIED, UPDATE_IMPORT_URL, WINDOW_RESIZED, ZEN_MODE_CHANGED } from '../constants/editor';
2 | import { changeZenMode, copyButtonHovered, copyText, diagramChanged, diagramHasError, diagramTypeChanged, importUrl, keyPressed, onWindowResized, openImportUrl, renderUrlChanged, updateUrl } from './editor'
3 | import delay from './utils/delay';
4 |
5 | import { resetCopy, hasCopy, getCopy, mockCopy } from './__jest__/copy'
6 | import { executeThunkAction, getDispatchActions, resetDispatchActions } from './__jest__/thunk'
7 | import { getCurrentTime } from './__jest__/time'
8 |
9 | jest.mock('copy-to-clipboard', () => (element) => { mockCopy(element); });
10 |
11 | describe('copyText', () => {
12 | it('should dispatch COPY_TEXT', async () => {
13 | resetCopy();
14 | resetDispatchActions();
15 | expect(hasCopy()).toBe(false);
16 | expect(getDispatchActions().length).toBe(0)
17 |
18 | const copyTextPromise = executeThunkAction(() => copyText('scope', 'text'))
19 |
20 | expect(hasCopy()).toBe(true);
21 | expect(getCopy()).toBe('text')
22 | let dispatch_state = getDispatchActions();
23 | expect(dispatch_state.length).toBe(2)
24 | expect(dispatch_state[0]).toStrictEqual({ type: COPY_TEXT, scope: 'scope', text: 'text' })
25 | expect(dispatch_state[1]).toStrictEqual({ type: TEXT_COPIED, scope: 'scope', isCopied: true })
26 |
27 | await copyTextPromise;
28 |
29 | dispatch_state = getDispatchActions();
30 | expect(dispatch_state.length).toBe(3)
31 | expect(dispatch_state[0]).toStrictEqual({ type: COPY_TEXT, scope: 'scope', text: 'text' })
32 | expect(dispatch_state[1]).toStrictEqual({ type: TEXT_COPIED, scope: 'scope', isCopied: true })
33 | expect(dispatch_state[2]).toStrictEqual({ type: TEXT_COPIED, scope: 'scope', isCopied: false })
34 | })
35 | })
36 |
37 | describe('copyButtonHovered', () => {
38 | const testForHover = (isHover) => {
39 | it(`should dispatch the correct action for isHover at ${isHover}`, () => {
40 | const result = copyButtonHovered('scope', isHover);
41 | expect(result).toStrictEqual({ type: COPY_BUTTON_HOVERED, scope: 'scope', isHover })
42 | })
43 | }
44 | testForHover(true)
45 | testForHover(false)
46 | })
47 |
48 | describe('renderUrlChanged', () => {
49 | it(`should dispatch the correct action`, () => {
50 | const result = renderUrlChanged('https://example.com/test');
51 | expect(result).toStrictEqual({ type: RENDERURL_CHANGED, renderUrl: 'https://example.com/test' })
52 | })
53 | })
54 |
55 | describe('diagramChanged', () => {
56 | it('should dispatch the correct actions with just one change', async () => {
57 | resetDispatchActions();
58 | let dispatch_state = getDispatchActions();
59 | expect(dispatch_state.length).toBe(0)
60 | const startTest = getCurrentTime();
61 |
62 | const diagramChangedPromise = executeThunkAction(() => diagramChanged('text 01'))
63 |
64 | dispatch_state = getDispatchActions();
65 | expect(dispatch_state.length).toBe(1)
66 | expect(dispatch_state[0]).toStrictEqual({ type: DIAGRAM_CHANGED, diagramText: 'text 01' })
67 |
68 | await diagramChangedPromise;
69 |
70 | const endTest = getCurrentTime();
71 |
72 | dispatch_state = getDispatchActions();
73 | expect(dispatch_state).toStrictEqual([
74 | { type: DIAGRAM_CHANGED, diagramText: 'text 01' },
75 | { type: DIAGRAM_CHANGED_UPDATE },
76 | ])
77 | expect(endTest - startTest).toBeGreaterThanOrEqual(750)
78 | })
79 |
80 | it('should dispatch the correct actions with several changes including some with less than 750ms interval', async () => {
81 | resetDispatchActions();
82 | let dispatch_state = getDispatchActions();
83 | expect(dispatch_state.length).toBe(0)
84 |
85 | const startTest01 = getCurrentTime();
86 | const diagramChangedPromise01 = executeThunkAction(() => diagramChanged('text 01'))
87 |
88 | dispatch_state = getDispatchActions();
89 | expect(dispatch_state.length).toBe(1)
90 | expect(dispatch_state[0]).toStrictEqual({ type: DIAGRAM_CHANGED, diagramText: 'text 01' })
91 |
92 | await delay(100);
93 |
94 | const startTest02 = getCurrentTime();
95 | const diagramChangedPromise02 = executeThunkAction(() => diagramChanged('text 02'))
96 |
97 | dispatch_state = getDispatchActions();
98 | expect(dispatch_state.length).toBe(2)
99 | expect(dispatch_state[1]).toStrictEqual({ type: DIAGRAM_CHANGED, diagramText: 'text 02' })
100 |
101 | await delay(300);
102 |
103 | const startTest03 = getCurrentTime();
104 | const diagramChangedPromise03 = executeThunkAction(() => diagramChanged('text 03'))
105 |
106 | dispatch_state = getDispatchActions();
107 | expect(dispatch_state.length).toBe(3)
108 | expect(dispatch_state[2]).toStrictEqual({ type: DIAGRAM_CHANGED, diagramText: 'text 03' })
109 |
110 | await delay(800);
111 |
112 | const startTest04 = getCurrentTime();
113 | const diagramChangedPromise04 = executeThunkAction(() => diagramChanged('text 04'))
114 |
115 | dispatch_state = getDispatchActions();
116 | expect(dispatch_state.length).toBe(5)
117 | expect(dispatch_state[3]).toStrictEqual({ type: DIAGRAM_CHANGED_UPDATE })
118 | expect(dispatch_state[4]).toStrictEqual({ type: DIAGRAM_CHANGED, diagramText: 'text 04' })
119 |
120 | await delay(100);
121 |
122 | const startTest05 = getCurrentTime();
123 | const diagramChangedPromise05 = executeThunkAction(() => diagramChanged('text 05'))
124 |
125 | dispatch_state = getDispatchActions();
126 | expect(dispatch_state.length).toBe(6)
127 | expect(dispatch_state[5]).toStrictEqual({ type: DIAGRAM_CHANGED, diagramText: 'text 05' })
128 |
129 | await Promise.all([
130 | diagramChangedPromise01,
131 | diagramChangedPromise02,
132 | diagramChangedPromise03,
133 | diagramChangedPromise04,
134 | diagramChangedPromise05
135 | ]);
136 |
137 | const endTest = getCurrentTime();
138 |
139 | dispatch_state = getDispatchActions();
140 | expect(dispatch_state).toStrictEqual([
141 | { type: DIAGRAM_CHANGED, diagramText: 'text 01' },
142 | { type: DIAGRAM_CHANGED, diagramText: 'text 02' },
143 | { type: DIAGRAM_CHANGED, diagramText: 'text 03' },
144 | { type: DIAGRAM_CHANGED_UPDATE },
145 | { type: DIAGRAM_CHANGED, diagramText: 'text 04' },
146 | { type: DIAGRAM_CHANGED, diagramText: 'text 05' },
147 | { type: DIAGRAM_CHANGED_UPDATE },
148 | ])
149 | expect(startTest02 - startTest01).toBeLessThan(750)
150 | expect(startTest03 - startTest02).toBeLessThan(750)
151 | expect(startTest04 - startTest03).toBeGreaterThanOrEqual(750)
152 | expect(startTest05 - startTest04).toBeLessThan(750)
153 | expect(endTest - startTest05).toBeGreaterThanOrEqual(750)
154 | })
155 | })
156 |
157 | describe('diagramTypeChanged', () => {
158 | it(`should dispatch the correct action`, () => {
159 | const result = diagramTypeChanged('grutUML');
160 | expect(result).toStrictEqual({ type: DIAGRAM_TYPE_CHANGED, diagramType: 'grutUML' })
161 | })
162 | })
163 |
164 | describe('importUrl', () => {
165 | it(`should dispatch the correct action`, () => {
166 | const result = importUrl('https://example.com/diagramType/data==');
167 | expect(result).toStrictEqual({ type: IMPORT_URL, url: 'https://example.com/diagramType/data==' })
168 | })
169 | })
170 |
171 | describe('closeImportUrl', () => {
172 | it(`should dispatch the correct action`, () => {
173 | const result = importUrl('https://example.com/diagramType/data==');
174 | expect(result).toStrictEqual({ type: IMPORT_URL, url: 'https://example.com/diagramType/data==' })
175 | })
176 | })
177 |
178 | describe('openImportUrl', () => {
179 | it(`should dispatch the correct action`, () => {
180 | const result = openImportUrl();
181 | expect(result).toStrictEqual({ type: OPEN_IMPORT_URL })
182 | })
183 | })
184 |
185 | describe('updateUrl', () => {
186 | it(`should dispatch the correct action`, () => {
187 | const result = updateUrl('https://example.com/diagramType/data==');
188 | expect(result).toStrictEqual({ type: UPDATE_IMPORT_URL, url: 'https://example.com/diagramType/data==' })
189 | })
190 | })
191 |
192 | describe('diagramHasError', () => {
193 | it(`should dispatch the correct action`, () => {
194 | const result = diagramHasError('https://example.com/diagramType/data==');
195 | expect(result).toStrictEqual({ type: DIAGRAM_HAS_ERROR, url: 'https://example.com/diagramType/data==' })
196 | })
197 | })
198 |
199 | describe('changeZenMode', () => {
200 | it(`should dispatch the correct action`, () => {
201 | const result = changeZenMode(true);
202 | expect(result).toStrictEqual({ type: ZEN_MODE_CHANGED, zenMode: true })
203 | })
204 | })
205 |
206 | describe('keyPressed', () => {
207 | it(`should dispatch the correct action`, () => {
208 | const result = keyPressed({ code: 'KeyQ', key: 'a', ctrlKey: false, shiftKey: false, altKey: true, metaKey: false });
209 | expect(result).toStrictEqual({ type: KEY_PRESSED, code: 'KeyQ', key: 'a', ctrlKey: false, shiftKey: false, altKey: true, metaKey: false })
210 | })
211 | })
212 |
213 | describe('onWindowResized', () => {
214 | it(`should dispatch the correct action`, () => {
215 | const result = onWindowResized(1920, 1080);
216 | expect(result).toStrictEqual({ type: WINDOW_RESIZED, width: 1920, height: 1080 })
217 | })
218 | })
219 |
220 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Niolesk
2 |
3 | Edit **diagrams** from **textual** descriptions! : A [kroki](https://kroki.io/) interface.
4 |
5 | * Application : https://niolesk.top/
6 | * Project page : https://github.com/webgiss/niolesk/
7 | * Container image : `ghcr.io/webgiss/niolesk` (static site served by nginx)
8 |
9 | ## Description
10 |
11 | Provide an interface for https://kroki.io/
12 |
13 | Just add any kroki url after the url of niolesk followed by `#` and you'll be able to edit the diagram, and export again a new url after change.
14 |
15 | ## Example
16 |
17 | 
18 |
19 | [Edit this diagram](https://niolesk.top/#https://kroki.io/svgbob/svg/eNpdzLEJADEMA8A-U6j7PMFO71kMyga_gIeP3L5BUnFg4H_kWK4tfKAabj4bYo9CNtTkKyh7cGwhUoDcTQp5zPSmEMgLdawUdw==)
20 |
21 | ## Docker
22 |
23 | To start a demo site:
24 |
25 | ```
26 | docker run -d --rm=true -p 8017:80 ghcr.io/webgiss/niolesk
27 | ```
28 |
29 | Or using docker-compose:
30 |
31 | ```yaml
32 | version: "3.5"
33 | services:
34 | niolesk:
35 | image: "ghcr.io/webgiss/niolesk"
36 | ports:
37 | - "8017:80"
38 | hostname: "niolesk"
39 | restart: "always"
40 | ```
41 |
42 | Then go to http://127.0.0.1:8017/ and it's running.
43 |
44 | ### Unprivileged images
45 |
46 | Standard images are based on nginx:latest image. If you need unprivileged images, you can use
47 | * `ghcr.io/webgiss/niolesk:unprivileged` instead of `ghcr.io/webgiss/niolesk:latest`
48 | * `ghcr.io/webgiss/niolesk:unprivileged-`{version} instead of `ghcr.io/webgiss/niolesk:`{version}
49 |
50 | Those images are based on image provided by https://github.com/nginxinc/docker-nginx-unprivileged
51 |
52 | Note: The port in the container isn't 80 but 8080 (as 80 is only accessible to privileged user).
53 |
54 | So the standard demo site becomes:
55 |
56 | ```
57 | docker run -d --rm=true -p 8017:8080 -u 1037:1037 ghcr.io/webgiss/niolesk:unprivileged
58 | ```
59 |
60 | ### Docker platforms
61 |
62 | Image is currently available for various platforms:
63 | * linux/amd64
64 | * linux/arm64
65 | * linux/386
66 | * linux/arm/v7
67 |
68 | ### Advanced usage
69 |
70 | If you want your niolesk docker instance to be linked to your kroki docker instance (hosted at https://kroki.example.com/) just start with command line:
71 |
72 | ```
73 | docker run -d --rm=true -e "NIOLESK_KROKI_ENGINE=https://kroki.example.com/" -p 8017:80 ghcr.io/webgiss/niolesk
74 | ```
75 |
76 | Or using docker-compose:
77 |
78 | ```yaml
79 | version: "3.5"
80 | services:
81 | niolesk:
82 | image: "ghcr.io/webgiss/niolesk"
83 | ports:
84 | - "8017:80"
85 | hostname: "niolesk"
86 | restart: "always"
87 | environment:
88 | - "NIOLESK_KROKI_ENGINE=https://kroki.example.com/"
89 | ```
90 |
91 | ### Analytics links
92 |
93 | You can configure analytics provider using docker env var.
94 |
95 | #### Matomo
96 |
97 | You can use "matomo_js" or "matomo_image" as analytics providers, and then use matomo url as arg1 and matomo site id as arg2
98 |
99 | ```
100 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_PROVIDER_NAME=matomo_js" -e "NIOLESK_ANALYTICS_PROVIDER_ARG1=https://matomo.example.com/" -e "NIOLESK_ANALYTICS_PROVIDER_ARG2=32" -p 8017:80 ghcr.io/webgiss/niolesk
101 | ```
102 |
103 | Or using docker-compose:
104 |
105 | ```yaml
106 | version: "3.5"
107 | services:
108 | niolesk:
109 | image: "ghcr.io/webgiss/niolesk"
110 | ports:
111 | - "8017:80"
112 | hostname: "niolesk"
113 | restart: "always"
114 | environment:
115 | - "NIOLESK_ANALYTICS_PROVIDER_NAME=matomo_js"
116 | - "NIOLESK_ANALYTICS_PROVIDER_ARG1=https://matomo.example.com/"
117 | - "NIOLESK_ANALYTICS_PROVIDER_ARG2=32"
118 | ```
119 |
120 | #### Google analytics 4
121 |
122 | You can use "google_ga4" as analytics providers, and then use the measurement id as arg1
123 |
124 | ```
125 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_PROVIDER_NAME=google_ga4" -e "NIOLESK_ANALYTICS_PROVIDER_ARG1=G-XXX999XXXX" -p 8017:80 ghcr.io/webgiss/niolesk
126 | ```
127 |
128 | Or using docker-compose:
129 |
130 | ```yaml
131 | version: "3.5"
132 | services:
133 | niolesk:
134 | image: "ghcr.io/webgiss/niolesk"
135 | ports:
136 | - "8017:80"
137 | hostname: "niolesk"
138 | restart: "always"
139 | environment:
140 | - "NIOLESK_ANALYTICS_PROVIDER_NAME=google_ga4"
141 | - "NIOLESK_ANALYTICS_PROVIDER_ARG1=G-XXX999XXXX"
142 | ```
143 |
144 | #### Google tag manager
145 |
146 | You can use "google_tag" as analytics providers, and then use the tag id as arg1
147 |
148 | ```
149 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_PROVIDER_NAME=google_tag" -e "NIOLESK_ANALYTICS_PROVIDER_ARG1=GTM-X9XXXXX" -p 8017:80 ghcr.io/webgiss/niolesk
150 | ```
151 |
152 | Or using docker-compose:
153 |
154 | ```yaml
155 | version: "3.5"
156 | services:
157 | niolesk:
158 | image: "ghcr.io/webgiss/niolesk"
159 | ports:
160 | - "8017:80"
161 | hostname: "niolesk"
162 | restart: "always"
163 | environment:
164 | - "NIOLESK_ANALYTICS_PROVIDER_NAME=google_tag"
165 | - "NIOLESK_ANALYTICS_PROVIDER_ARG1=GTM-X9XXXXX"
166 | ```
167 |
168 | #### Any analytics using only docker env vars
169 |
170 | You can use any analytics providers either by providing "just" type/content
171 |
172 | ```
173 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_TYPE=html" -e 'NIOLESK_ANALYTICS_CONTENT=
' -p 8017:80 ghcr.io/webgiss/niolesk
174 | ```
175 |
176 | Or using docker-compose:
177 |
178 | ```yaml
179 | version: "3.5"
180 | services:
181 | niolesk:
182 | image: "ghcr.io/webgiss/niolesk"
183 | ports:
184 | - "8017:80"
185 | hostname: "niolesk"
186 | restart: "always"
187 | environment:
188 | - "NIOLESK_ANALYTICS_TYPE=html"
189 | - 'NIOLESK_ANALYTICS_CONTENT=
'
190 | ```
191 |
192 | Note that you can't provide direct html tag `` in your HTML content as there are [security restrictions](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations). You have to specify explicit script content as type `js` by calling:
193 |
194 | ```
195 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_TYPE=js" -e 'NIOLESK_ANALYTICS_CONTENT=console.log("This is analytics code")' -p 8017:80 ghcr.io/webgiss/niolesk
196 | ```
197 |
198 | Or using docker-compose:
199 |
200 | ```yaml
201 | version: "3.5"
202 | services:
203 | niolesk:
204 | image: "ghcr.io/webgiss/niolesk"
205 | ports:
206 | - "8017:80"
207 | hostname: "niolesk"
208 | restart: "always"
209 | environment:
210 | - "NIOLESK_ANALYTICS_TYPE=js"
211 | - 'NIOLESK_ANALYTICS_CONTENT=console.log("This is analytics code")'
212 | ```
213 |
214 | #### Any analytics by creating your own provider
215 |
216 | You can also create your own analytics provider by including your own version of file `/usr/share/nginx/html/config-analytic-providers.js` inside docker:
217 |
218 | Create a file where you want, for example `/opt/niolesk/config-analytic-providers.js`:
219 |
220 | ```js
221 | window.config_analytic_providers = {
222 | my_provider: [
223 | {
224 | type: 'js',
225 | content: `
226 | const _dataTracker = window._dataTracker = window._dataTracker || []
227 | const eTrack = (key,value) => _dataTracker.push([key,value])
228 | eTrack('now',new Date())
229 | eTrack('id','E-0145456533')
230 | `,
231 | },
232 | {
233 | type: 'js',
234 | content: 'https://example.com/analytics/script.js?id=E-0145456533',
235 | },
236 | ],
237 | }
238 | ```
239 |
240 | and then call
241 |
242 | ```
243 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_PROVIDER_NAME=my_provider" -v "/opt/niolesk/config-analytic-providers.js:/usr/share/nginx/html/config-analytic-providers.js:ro" -p 8017:80 ghcr.io/webgiss/niolesk
244 | ```
245 |
246 | Or using docker-compose:
247 |
248 | ```yaml
249 | version: "3.5"
250 | services:
251 | niolesk:
252 | image: "ghcr.io/webgiss/niolesk"
253 | ports:
254 | - "8017:80"
255 | hostname: "niolesk"
256 | restart: "always"
257 | volumes:
258 | - "/opt/niolesk/config-analytic-providers.js:/usr/share/nginx/html/config-analytic-providers.js:ro"
259 | environment:
260 | - "NIOLESK_ANALYTICS_PROVIDER_NAME=my_provider"
261 | ```
262 |
263 | But you can also use providers args if you want:
264 |
265 | `/opt/niolesk/config-analytic-providers.js`:
266 |
267 | ```js
268 | window.config_analytic_providers = {
269 | my_provider: [
270 | {
271 | type: 'js',
272 | content: `
273 | const _dataTracker = window._dataTracker = window._dataTracker || []
274 | const eTrack = (key,value) => _dataTracker.push([key,value])
275 | eTrack('now',new Date())
276 | eTrack('id','{1}')
277 | `,
278 | },
279 | {
280 | type: 'js',
281 | content: 'https://example.com/analytics/script.js?id={1}',
282 | },
283 | ],
284 | }
285 | ```
286 |
287 | ```
288 | docker run -d --rm=true -e "NIOLESK_ANALYTICS_PROVIDER_NAME=my_provider" -e "NIOLESK_ANALYTICS_PROVIDER_ARG1=E-0145456533" -v "/opt/niolesk/config-analytic-providers.js:/usr/share/nginx/html/config-analytic-providers.js:ro" -p 8017:80 ghcr.io/webgiss/niolesk
289 | ```
290 |
291 | Or using docker-compose:
292 |
293 | ```yaml
294 | version: "3.5"
295 | services:
296 | niolesk:
297 | image: "ghcr.io/webgiss/niolesk"
298 | ports:
299 | - "8017:80"
300 | hostname: "niolesk"
301 | restart: "always"
302 | volumes:
303 | - "/opt/niolesk/config-analytic-providers.js:/usr/share/nginx/html/config-analytic-providers.js:ro"
304 | environment:
305 | - "NIOLESK_ANALYTICS_PROVIDER_NAME=my_provider"
306 | - "NIOLESK_ANALYTICS_PROVIDER_ARG1=E-0145456533"
307 | ```
308 |
309 | ## Deploying the pages as a static site on your own server without using docker
310 |
311 | You can download the static pages of the latest release using the file `niolesk-site`-{version}.{extension}
312 |
313 | Every docker variable described can be configurd by editing lines in `config.js`. `config-analytic-providers.js` can be edited by simply... editing `config-analytic-providers.js`.
314 |
315 | ## Development
316 |
317 | The storybook of the project from the latest tag is accessible at https://niolesk.top/storybook
318 |
319 | Help for [compilation](compilation.md) of the project.
320 |
--------------------------------------------------------------------------------
/src/reducers/editor.js:
--------------------------------------------------------------------------------
1 | import { COPY_BUTTON_HOVERED, DIAGRAM_CHANGED, DIAGRAM_CHANGED_UPDATE, DIAGRAM_TYPE_CHANGED, RENDERURL_CHANGED, TEXT_COPIED, IMPORT_URL, CLOSE_IMPORT_URL, OPEN_IMPORT_URL, UPDATE_IMPORT_URL, DIAGRAM_HAS_ERROR, ZEN_MODE_CHANGED, KEY_PRESSED, WINDOW_RESIZED, RENDER_EDIT_SIZE_CHANGED } from "../constants/editor";
2 | import { createReducer } from "./utils/createReducer";
3 | import { encode, decode } from "../kroki/coder";
4 | import diagramTypes from "../kroki/krokiInfo";
5 | import { IMPORT_EXAMPLE } from "../constants/example";
6 | import { createKrokiUrl } from "../kroki/utils";
7 | import { LOCATION_CHANGE } from "connected-react-router";
8 | import exampleData from '../examples/data';
9 |
10 | const defaultDiagramType = 'plantuml';
11 |
12 | export const initialState = {
13 | baseUrl: window.location.origin + window.location.pathname,
14 | hash: null,
15 | diagramType: defaultDiagramType,
16 | diagramText: decode(diagramTypes[defaultDiagramType].example),
17 | filetype: 'svg',
18 | defaultDiagram: true,
19 | diagramTypes,
20 | language: null,
21 | renderUrl: (window.config && window.config.krokiEngineUrl) || 'https://kroki.io/',
22 | scopes: {
23 | 'image': {
24 | isHover: false,
25 | isCopied: false,
26 | },
27 | 'edit': {
28 | isHover: false,
29 | isCopied: false,
30 | },
31 | 'markdown': {
32 | isHover: false,
33 | isCopied: false,
34 | },
35 | 'markdownsource': {
36 | isHover: false,
37 | isCopied: false,
38 | },
39 | },
40 | windowImportUrlOpened: false,
41 | windowImportUrl: '',
42 | diagramError: false,
43 | zenMode: false,
44 | height: null,
45 | width: null,
46 | renderHeight: 700,
47 | editorHeight: 700,
48 | renderWidth: 800,
49 | renderEditHeight: 0,
50 | redrawIndex: 0,
51 | };
52 |
53 | const setWindowWidthHeight = (state, width, height) => {
54 | const { zenMode } = state
55 | const editorHeight = zenMode ? (width<768 ? height/2-14 : height) : 700
56 | const renderHeight = editorHeight - state.renderEditHeight
57 | let redrawIndex = state.redrawIndex
58 | if (renderHeight !== state.renderHeight) {
59 | redrawIndex += 1
60 | }
61 | if (width !== state.width || height !== state.height || renderHeight !== state.renderHeight || editorHeight !== state.editorHeight || redrawIndex !== state.redrawIndex) {
62 | return { ...state, width, height, renderHeight, editorHeight, redrawIndex }
63 | }
64 | return state
65 | }
66 |
67 | const setRenderWidth = (state, renderEditWidth, renderEditHeight) => {
68 | const renderWidth = renderEditWidth
69 | const renderHeight = state.editorHeight - renderEditHeight
70 | let redrawIndex = state.redrawIndex
71 | if (renderWidth !== state.renderWidth || renderHeight !== state.renderHeight) {
72 | redrawIndex += 1
73 | }
74 | if (renderWidth !== state.renderWidth || renderHeight !== state.renderHeight || renderEditHeight !== state.renderEditHeight || redrawIndex !== state.redrawIndex) {
75 | return { ...state, renderWidth, renderHeight, renderEditHeight, redrawIndex }
76 | }
77 | return state
78 | }
79 |
80 | /**
81 | *
82 | * @param {State} state
83 | * @returns
84 | */
85 | export const updateDiagram = (state) => {
86 | let { diagramType, filetype, renderUrl, diagramText, baseUrl, diagramTypes } = state;
87 | if (!renderUrl || renderUrl === '') {
88 | renderUrl = initialState.renderUrl;
89 | }
90 | if (!filetype || filetype === '') {
91 | filetype = initialState.filetype;
92 | }
93 | if (!diagramType || diagramType === '') {
94 | diagramType = state.diagramType;
95 | }
96 | if (!diagramType || diagramType === '') {
97 | diagramType = initialState.diagramType;
98 | }
99 | const language = diagramTypes[diagramType]?.language;
100 | const codedDiagramTextText = encode(diagramText);
101 | const defaultDiagram = exampleData.filter(({ diagramType: type, example }) => (diagramType === type) && (example === codedDiagramTextText)).length > 0;
102 | const diagramUrl = createKrokiUrl(renderUrl, diagramType, filetype, codedDiagramTextText);
103 | if (state.diagramUrl !== diagramUrl) {
104 | state = { ...state, diagramUrl, diagramEditUrl: `${baseUrl}#${diagramUrl}`, diagramError: false, defaultDiagram, language }
105 | }
106 | return state;
107 | }
108 |
109 | /**
110 | *
111 | * @param {State} state
112 | * @param {string} hash
113 | * @returns
114 | * @template State
115 | */
116 | export const updateHash = (state, hash) => {
117 | let url = hash;
118 | if (url.startsWith('#')) {
119 | url = url.substr(1);
120 | }
121 | let protocol = null;
122 | let renderSite = null;
123 | let coded = null;
124 | let filetype = null;
125 | let diagramType = null;
126 | let renderUrl = null;
127 | let diagramText = null;
128 | const protocolSeparator = '://';
129 | const protocolSeparatorPosition = url.indexOf(protocolSeparator);
130 | if (protocolSeparatorPosition >= 0) {
131 | protocol = url.substr(0, protocolSeparatorPosition);
132 | url = url.substr(protocolSeparatorPosition + protocolSeparator.length)
133 | }
134 | const urlParts = url.split('/');
135 | if (urlParts.length >= 4) {
136 | coded = urlParts[urlParts.length - 1];
137 | filetype = urlParts[urlParts.length - 2];
138 | diagramType = urlParts[urlParts.length - 3];
139 | if (urlParts.length > 3) {
140 | renderSite = urlParts.slice(0, urlParts.length - 3).join('/')
141 | }
142 | }
143 | if (renderSite && protocol) {
144 | renderUrl = protocol + protocolSeparator + renderSite;
145 | }
146 | if (coded) {
147 | diagramText = decode(coded);
148 | } else {
149 | diagramText = state.diagramText;
150 | }
151 | if (filetype === null) {
152 | filetype = state.filetype
153 | }
154 | if (diagramType === null) {
155 | diagramType = state.diagramType
156 | }
157 | if (renderUrl === null) {
158 | renderUrl = state.renderUrl
159 | }
160 | if (diagramText === null) {
161 | diagramText = state.diagramText
162 | }
163 | if (state.hash !== hash || state.filetype !== filetype || state.renderUrl !== renderUrl || state.diagramText !== diagramText) {
164 | state = { ...state, hash, filetype, diagramType, renderUrl, diagramText };
165 | state = updateDiagram(state);
166 | }
167 | return state;
168 | }
169 |
170 | const updateDiagramTypeAndTextIfDefault = (state, diagramType) => {
171 | if ((state.diagramText === '') || (state.defaultDiagram)) {
172 | state = { ...state, diagramType, diagramText: decode(state.diagramTypes[diagramType].example), defaultDiagram: true, redrawIndex: state.redrawIndex + 1 };
173 | } else {
174 | state = { ...state, diagramType, redrawIndex: state.redrawIndex + 1 };
175 | }
176 | state = updateDiagram(state);
177 | return state;
178 | }
179 |
180 | export default createReducer({
181 | [LOCATION_CHANGE]: (state, action) => {
182 | const { location, isFirstRendering } = action.payload;
183 | let hash = location.hash;
184 | if (hash === undefined) {
185 | hash = location.location.hash;
186 | }
187 | if (state.hash !== hash || isFirstRendering) {
188 | state = updateHash(state, hash);
189 | }
190 | return state;
191 | },
192 | [COPY_BUTTON_HOVERED]: (state, action) => {
193 | const { scope, isHover } = action;
194 | if (isHover !== state.scopes[scope].isHover) {
195 | state = { ...state, scopes: { ...state.scopes, [scope]: { ...state.scopes[scope], isHover } } }
196 | }
197 | return state;
198 | },
199 | [TEXT_COPIED]: (state, action) => {
200 | const { scope, isCopied } = action;
201 | if (isCopied !== state.scopes[scope].isCopied) {
202 | state = { ...state, scopes: { ...state.scopes, [scope]: { ...state.scopes[scope], isCopied } } }
203 | }
204 | return state;
205 | },
206 | [RENDERURL_CHANGED]: (state, action) => {
207 | const { renderUrl } = action;
208 | if (renderUrl !== state.renderUrl || !state.diagramUrl) {
209 | state = { ...state, renderUrl };
210 | state = updateDiagram(state);
211 | }
212 | return state;
213 | },
214 | [DIAGRAM_CHANGED]: (state, action) => {
215 | const { diagramText } = action;
216 | if (diagramText !== state.diagramText) {
217 | state = { ...state, diagramText };
218 | // state = updateDiagram(state);
219 | }
220 | return state;
221 | },
222 | [DIAGRAM_CHANGED_UPDATE]: (state) => {
223 | state = updateDiagram(state);
224 | return state;
225 | },
226 | [DIAGRAM_TYPE_CHANGED]: (state, action) => {
227 | const { diagramType } = action;
228 | if (diagramType !== state.diagramType) {
229 | state = updateDiagramTypeAndTextIfDefault(state, diagramType);
230 | }
231 | return state;
232 | },
233 | [IMPORT_EXAMPLE]: (state, action) => {
234 | const { diagramText, diagramType } = action;
235 | if ((diagramText !== state.diagramText) || (diagramType !== state.diagramType)) {
236 | state = updateDiagram({ ...state, diagramText, diagramType })
237 | state = { ...state, redrawIndex: state.redrawIndex + 1 }
238 | }
239 | return state;
240 | },
241 | [IMPORT_URL]: (state, action) => {
242 | const { url } = action;
243 | if (url && url !== '') {
244 | state = { ...state, windowImportUrlOpened: false, windowImportUrl: '' };
245 | state = updateHash(state, url);
246 | }
247 | return state;
248 | },
249 | [CLOSE_IMPORT_URL]: (state) => {
250 | state = { ...state, windowImportUrlOpened: false, windowImportUrl: '' };
251 | return state;
252 | },
253 | [OPEN_IMPORT_URL]: (state) => {
254 | state = { ...state, windowImportUrlOpened: true, windowImportUrl: '' };
255 | return state;
256 | },
257 | [UPDATE_IMPORT_URL]: (state, action) => {
258 | const { url } = action;
259 | state = { ...state, windowImportUrl: url };
260 | return state;
261 | },
262 | [DIAGRAM_HAS_ERROR]: (state, action) => {
263 | const { url } = action;
264 | if (url === state.diagramUrl) {
265 | state = { ...state, diagramError: true }
266 | }
267 | return state;
268 | },
269 | [ZEN_MODE_CHANGED]: (state, action) => {
270 | const { zenMode } = action;
271 | if (zenMode !== state.zenMode) {
272 | state = { ...state, zenMode }
273 | }
274 | return state;
275 | },
276 | [KEY_PRESSED]: (state, action) => {
277 | const { key, ctrlKey, shiftKey, altKey, metaKey } = action
278 | if (key === 'Escape' && (!ctrlKey) && (!shiftKey) && (!altKey) && (!metaKey)) {
279 | if (state.zenMode) {
280 | state = { ...state, zenMode: false }
281 | }
282 | }
283 | if (key === 'z' && (!ctrlKey) && (!shiftKey) && (altKey) && (!metaKey)) {
284 | if (!state.zenMode) {
285 | state = { ...state, zenMode: true }
286 | }
287 | }
288 | if (key === 'i' && (!ctrlKey) && (!shiftKey) && (altKey) && (!metaKey)) {
289 | if (!state.windowImportUrlOpened) {
290 | state = { ...state, windowImportUrlOpened: true, windowImportUrl: '' };
291 | }
292 | }
293 | return state;
294 | },
295 | [WINDOW_RESIZED]: (state, action) => {
296 | const { width, height } = action
297 | return setWindowWidthHeight(state, width, height)
298 | },
299 | [RENDER_EDIT_SIZE_CHANGED]: (state, action) => {
300 | const { renderEditWidth, renderEditHeight } = action
301 | return setRenderWidth(state, renderEditWidth, renderEditHeight)
302 | },
303 | }, initialState);
304 |
--------------------------------------------------------------------------------