├── .gitignore
├── src
├── react-app-env.d.ts
├── utils
│ ├── makeid.ts
│ ├── markdown.ts
│ └── showDialog.tsx
├── lib
│ └── classes
│ │ ├── SerializedRange.ts
│ │ ├── errors
│ │ └── DeserializationError.ts
│ │ ├── Context.ts
│ │ ├── HighlightPainter.ts
│ │ ├── EventHandler.ts
│ │ └── Marker.ts
├── index.tsx
├── Components
│ └── MarkdownRender
│ │ ├── overrides
│ │ ├── index.js
│ │ ├── math
│ │ │ └── index.jsx
│ │ └── pre
│ │ │ └── index.jsx
│ │ ├── MarkdownRender.scss
│ │ ├── IframeRender.jsx
│ │ └── MarkdownRender.jsx
├── libroot.ts
├── App.tsx
└── test.md
├── design
├── demo.png
└── logo
│ ├── FullColor_1280x1024.pdf
│ ├── FullColor_1280x1024_300dpi.jpg
│ ├── FullColor_1280x1024_72dpi.jpg
│ ├── FullColor_1280x1024_72dpi.png
│ ├── Grayscale_1280x1024_72dpi.png
│ ├── website_logo_solid_background.png
│ ├── FullColor_IconOnly_1280x1024_72dpi.jpg
│ ├── FullColor_TextOnly_1280x1024_72dpi.jpg
│ ├── website_logo_transparent_background.png
│ ├── FullColor_TransparentBg_1280x1024_72dpi.png
│ ├── FullColor_1280x1024.svg
│ └── FullColor_1280x1024.eps
├── public
├── manifest.json
├── index.html
├── iframe.html
└── logo.svg
├── .npmignore
├── tsconfig.gen-dts.json
├── tsconfig.json
├── LICENSE
├── config-overrides.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | build
4 | /*.tgz
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/design/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/demo.png
--------------------------------------------------------------------------------
/design/logo/FullColor_1280x1024.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_1280x1024.pdf
--------------------------------------------------------------------------------
/src/utils/makeid.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 |
3 | export default function makeid() {
4 | return uuidv4();
5 | }
6 |
--------------------------------------------------------------------------------
/design/logo/FullColor_1280x1024_300dpi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_1280x1024_300dpi.jpg
--------------------------------------------------------------------------------
/design/logo/FullColor_1280x1024_72dpi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_1280x1024_72dpi.jpg
--------------------------------------------------------------------------------
/design/logo/FullColor_1280x1024_72dpi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_1280x1024_72dpi.png
--------------------------------------------------------------------------------
/design/logo/Grayscale_1280x1024_72dpi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/Grayscale_1280x1024_72dpi.png
--------------------------------------------------------------------------------
/design/logo/website_logo_solid_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/website_logo_solid_background.png
--------------------------------------------------------------------------------
/design/logo/FullColor_IconOnly_1280x1024_72dpi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_IconOnly_1280x1024_72dpi.jpg
--------------------------------------------------------------------------------
/design/logo/FullColor_TextOnly_1280x1024_72dpi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_TextOnly_1280x1024_72dpi.jpg
--------------------------------------------------------------------------------
/design/logo/website_logo_transparent_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/website_logo_transparent_background.png
--------------------------------------------------------------------------------
/design/logo/FullColor_TransparentBg_1280x1024_72dpi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notelix/web-marker/HEAD/design/logo/FullColor_TransparentBg_1280x1024_72dpi.png
--------------------------------------------------------------------------------
/src/lib/classes/SerializedRange.ts:
--------------------------------------------------------------------------------
1 | interface SerializedRange {
2 | uid: string;
3 | textBefore: string;
4 | text: string;
5 | textAfter: string;
6 | }
7 |
8 | export default SerializedRange;
9 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Web Marker",
3 | "name": "",
4 | "icons": [],
5 | "start_url": ".",
6 | "display": "standalone",
7 | "theme_color": "#000000",
8 | "background_color": "#ffffff"
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/classes/errors/DeserializationError.ts:
--------------------------------------------------------------------------------
1 | export default class DeserializationError extends Error {}
2 |
3 | export function isDeserializationError(err: Error): boolean {
4 | return err instanceof DeserializationError;
5 | }
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | public
3 | src
4 | *.md
5 | *.txt
6 | *.jpg
7 | *.png
8 | *.ico
9 | *.tgz
10 | test
11 | dist
12 | config-overrides.js
13 | manifest.json
14 | design
15 | build/logo.svg
16 | tsconfig.json
17 | tsconfig.gen-dts.json
18 |
--------------------------------------------------------------------------------
/src/lib/classes/Context.ts:
--------------------------------------------------------------------------------
1 | import SerializedRange from "./SerializedRange";
2 | import Marker from "./Marker";
3 |
4 | interface Context {
5 | serializedRange: SerializedRange;
6 | marker: Marker;
7 | }
8 |
9 | export default Context;
10 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById("root")
10 | );
11 |
--------------------------------------------------------------------------------
/src/Components/MarkdownRender/overrides/index.js:
--------------------------------------------------------------------------------
1 | export default (props) => ({
2 | pre: require("./pre").default,
3 | math: require("./math").default,
4 |
5 | table: {
6 | props: {
7 | className:
8 | "table is-bordered is-striped is-narrow is-hoverable is-fullwidth",
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/src/lib/classes/HighlightPainter.ts:
--------------------------------------------------------------------------------
1 | import Context from "./Context";
2 |
3 | interface HighlightPainter {
4 | paintHighlight: (context: Context, element: HTMLElement) => void;
5 | beforePaintHighlight?: (context: Context) => void;
6 | afterPaintHighlight?: (context: Context) => void;
7 | }
8 |
9 | export default HighlightPainter;
10 |
--------------------------------------------------------------------------------
/src/lib/classes/EventHandler.ts:
--------------------------------------------------------------------------------
1 | import Context from "./Context";
2 |
3 | interface EventHandler {
4 | onHighlightClick?: (context: Context, element: HTMLElement, e: Event) => void;
5 | onHighlightHoverStateChange?: (
6 | context: Context,
7 | element: HTMLElement,
8 | hovering: boolean,
9 | e: Event
10 | ) => void;
11 | }
12 |
13 | export default EventHandler;
14 |
--------------------------------------------------------------------------------
/tsconfig.gen-dts.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "emitDeclarationOnly": true,
5 | "outDir": "build/",
6 | "jsx": "react",
7 | "lib": [
8 | "es2017",
9 | "dom"
10 | ],
11 | "module": "commonjs",
12 | "moduleResolution": "node",
13 | "skipLibCheck": true,
14 | "strict": true,
15 | "strictNullChecks": false,
16 | "target": "es5"
17 | },
18 | "include": [
19 | "./src/libroot.ts",
20 | "./src/lib/*",
21 | "./src/lib/**/*"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/Components/MarkdownRender/overrides/math/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "katex/dist/katex.min.css";
3 | import { InlineMath, BlockMath } from "react-katex";
4 |
5 | import { isArray } from "lodash-es";
6 |
7 | function getChildren(props) {
8 | if (typeof props.children === "string") {
9 | return props.children;
10 | }
11 | if (isArray(props.children)) {
12 | return props.children[0];
13 | }
14 | return props.children;
15 | }
16 |
17 | const Math = (props) => {
18 | if (props.kind === "block") {
19 | return ;
20 | } else {
21 | return ;
22 | }
23 | };
24 |
25 | export default Math;
26 |
--------------------------------------------------------------------------------
/src/libroot.ts:
--------------------------------------------------------------------------------
1 | import Marker, { MarkerConstructorArgs } from "./lib/classes/Marker";
2 | import DeserializationError, {
3 | isDeserializationError,
4 | } from "./lib/classes/errors/DeserializationError";
5 | import SerializedRange from "./lib/classes/SerializedRange";
6 | import Context from "./lib/classes/Context";
7 | import EventHandler from "./lib/classes/EventHandler";
8 | import HighlightPainter from "./lib/classes/HighlightPainter";
9 |
10 | const Errors = {
11 | isDeserializationError,
12 | };
13 |
14 | export { Marker };
15 | export { Errors };
16 |
17 | export type {
18 | Context,
19 | EventHandler,
20 | HighlightPainter,
21 | SerializedRange,
22 | MarkerConstructorArgs,
23 | DeserializationError,
24 | };
25 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | // @ts-ignore
3 | // eslint-disable-next-line import/no-webpack-loader-syntax
4 | import testMd from "!raw-loader!./test.md";
5 | import MarkdownRender from "./Components/MarkdownRender/MarkdownRender";
6 | import IframeRender from "./Components/MarkdownRender/IframeRender";
7 |
8 | class App extends React.Component {
9 | private isIframe: boolean;
10 |
11 | constructor(props: any) {
12 | super(props)
13 | const urlSearchParams = new URLSearchParams(window.location.search.slice(1));
14 | this.isIframe = urlSearchParams.has('iframe');
15 | }
16 |
17 | render() {
18 | return this.isIframe
19 | ?
20 | : ;
21 | }
22 | }
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/utils/markdown.ts:
--------------------------------------------------------------------------------
1 | import yaml from "yaml";
2 |
3 | function parseMarkdown(text: any) {
4 | const lines = text.trim().split("\n");
5 | let inMeta = false;
6 |
7 | const metaLines = [];
8 | const contentLines = [];
9 |
10 | for (let line of lines) {
11 | if (inMeta) {
12 | if (line.startsWith("---")) {
13 | inMeta = false;
14 | continue;
15 | }
16 |
17 | metaLines.push(line);
18 | } else {
19 | if (!contentLines.length && line.startsWith("---")) {
20 | inMeta = true;
21 | continue;
22 | }
23 | contentLines.push(line);
24 | }
25 | }
26 |
27 | return {
28 | meta: yaml.parse(metaLines.join("\n")) || {},
29 | content: contentLines.join("\n").trim(),
30 | };
31 | }
32 |
33 | export { parseMarkdown };
34 |
--------------------------------------------------------------------------------
/src/Components/MarkdownRender/overrides/pre/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
3 | import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
4 | import docco from "react-syntax-highlighter/dist/esm/styles/hljs/docco";
5 | import Math from "../math";
6 |
7 | SyntaxHighlighter.registerLanguage("javascript", js);
8 |
9 | const Pre = (props) => {
10 | function getLang(props) {
11 | if (!!props.className && props.className.startsWith("lang-")) {
12 | return props.className.replace("lang-", "");
13 | }
14 | return "";
15 | }
16 |
17 | if (props.children.type === "code") {
18 | const childProps = props.children.props;
19 | let lang = getLang(childProps);
20 | if (lang === "math") {
21 | return ;
22 | }
23 |
24 | return (
25 |
26 | {childProps.children}
27 |
28 | );
29 | }
30 |
31 | return
ERROR
;
32 | };
33 |
34 | export default Pre;
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ke Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
2 | const path = require("path");
3 | const CopyPlugin = require("copy-webpack-plugin");
4 |
5 | module.exports = function override(config, env) {
6 | config.plugins.push(new CopyPlugin({
7 | patterns: [
8 | {from: 'public', to: 'public'}
9 | ]
10 | }))
11 | if (process.env.NO_BUNDLE_ANALYSER !== "true") {
12 | config.plugins.push(new BundleAnalyzerPlugin());
13 | }
14 | if (process.env.BUILD_TARGET === "lib") {
15 | config.entry = [
16 | path.resolve(process.cwd(), "src", "libroot.ts")
17 | ]
18 |
19 | config.output = {
20 | path: config.output.path,
21 | filename: 'web-marker.js',
22 | library: "web-marker",
23 | libraryTarget: "umd",
24 | }
25 |
26 | delete config.optimization["splitChunks"];
27 | delete config.optimization["runtimeChunk"];
28 | config.externals = ["react", "react-dom"];
29 | config.plugins = config.plugins.filter(p => ["HtmlWebpackPlugin", "GenerateSW", "ManifestPlugin"].indexOf(p.constructor.name) < 0)
30 | }
31 | return config;
32 | }
33 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 |
17 |
26 | web-marker
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@notelix/web-marker",
3 | "version": "2.0.6",
4 | "description": "a web page highlighter",
5 | "main": "./build/web-marker.js",
6 | "types": "./build/libroot.d.ts",
7 | "scripts": {
8 | "start": "react-app-rewired start",
9 | "build": "react-app-rewired build",
10 | "build-lib": "BUILD_TARGET=lib react-app-rewired build",
11 | "prepublish": "NO_BUNDLE_ANALYSER=true npm run build-lib; tsc -p tsconfig.gen-dts.json",
12 | "test": "react-app-rewired test",
13 | "eject": "react-app-rewired eject"
14 | },
15 | "dependencies": {
16 | "uuid": "^8.3.2"
17 | },
18 | "homepage": "https://github.com/notelix/web-marker",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/notelix/web-marker"
22 | },
23 | "devDependencies": {
24 | "@material-ui/core": "^4.11.0",
25 | "@types/react-dom": "^17.0.9",
26 | "@types/uuid": "^8.3.1",
27 | "bulma": "^0.9.1",
28 | "copy-webpack-plugin": "6",
29 | "lodash-es": "^4.17.15",
30 | "markdown-to-jsx": "^7.0.1",
31 | "raw-loader": "^4.0.1",
32 | "react": "^17.0.0",
33 | "react-app-rewired": "^2.1.6",
34 | "react-dom": "^17.0.0",
35 | "react-katex": "^2.0.2",
36 | "react-scripts": "^3.4.1",
37 | "react-syntax-highlighter": "^15.3.0",
38 | "sass": "^1.38.0",
39 | "typescript": "^4.0.5",
40 | "webpack-bundle-analyzer": "^4.4.2",
41 | "yaml": "^1.10.0"
42 | },
43 | "keywords": [
44 | "web",
45 | "marker",
46 | "text",
47 | "highlight",
48 | "annotation",
49 | "selection",
50 | "range"
51 | ],
52 | "author": "Ke Wang <453587854@qq.com>",
53 | "license": "MIT",
54 | "browserslist": {
55 | "production": [
56 | ">0.2%",
57 | "not dead",
58 | "not op_mini all"
59 | ],
60 | "development": [
61 | "last 1 chrome version",
62 | "last 1 firefox version",
63 | "last 1 safari version"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # web-marker
2 |
3 | 
4 |
5 | A web page highlighter that features
6 | * accurate serialization and deserialization which makes it possible to correctly restore the highlights, even if part of the web page has changed
7 | * nested highlighting
8 | * no runtime-dependency
9 |
10 | 
11 |
12 | # How to run
13 | ```bash
14 | git clone https://github.com/notelix/web-marker
15 | cd web-marker
16 | npm i
17 | npm start
18 | ```
19 |
20 | # How to use
21 | ```
22 | npm install @notelix/web-marker
23 | ```
24 |
25 | ```javascript
26 | import {Marker} from "@notelix/web-marker"
27 |
28 | const marker = new Marker({
29 | rootElement: document.body,
30 | eventHandler: {
31 | onHighlightClick: (context, element) => {
32 | marker.unpaint(context.serializedRange);
33 | },
34 | onHighlightHoverStateChange: (context, element, hovering) => {
35 | if (hovering) {
36 | element.style.backgroundColor = "#f0d8ff";
37 | } else {
38 | element.style.backgroundColor = "";
39 | }
40 | }
41 | },
42 | highlightPainter: {
43 | paintHighlight: (context, element) => {
44 | element.style.color = "red";
45 | }
46 | }
47 | });
48 |
49 | marker.addEventListeners();
50 |
51 | document.addEventListener('mouseup', (e) => {
52 | const selection = window.getSelection();
53 | if (!selection.rangeCount) {
54 | return null;
55 | }
56 | const serialized = marker.serializeRange(selection.getRangeAt(0));
57 | console.log(JSON.stringify(serialized));
58 | marker.paint(serialized);
59 | })
60 | ```
61 |
62 | # How to build library
63 | ```
64 | npm run build-lib
65 | npm pack
66 | ```
67 |
68 | # Built with web-marker
69 |
70 | * [notelix/notelix](https://github.com/notelix/notelix): An open source web note taking / highlighter software (chrome extension with backend)
71 |
--------------------------------------------------------------------------------
/src/utils/showDialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import ReactDOM from "react-dom";
3 | import Dialog from "@material-ui/core/Dialog";
4 | import DialogTitle from "@material-ui/core/DialogTitle";
5 | import DialogContent from "@material-ui/core/DialogContent";
6 | import DialogActions from "@material-ui/core/DialogActions";
7 | import Button from "@material-ui/core/Button";
8 |
9 | function getDialogButtons(dialogInstance: any, context: any) {
10 | if (dialogInstance.DialogButtons) {
11 | return dialogInstance.DialogButtons(context);
12 | } else {
13 | const { closeDialog } = context;
14 | return (
15 | closeDialog()} variant="contained">
16 | Close
17 |
18 | );
19 | }
20 | }
21 |
22 | function showDialog(DialogClass: any, props: any) {
23 | return new Promise((resolve) => {
24 | const dialogRoot = document.createElement("div");
25 | let clearUp = null as any;
26 | document.body.appendChild(dialogRoot);
27 |
28 | const Root = () => {
29 | const [open, setOpen] = useState(true);
30 | const [dialogButtons, setDialogButtons] = useState(null);
31 |
32 | const dialogInstance = React.createElement(DialogClass, {
33 | ...(props || {}),
34 | ref: (instance: any) => {
35 | if (!instance || dialogButtons) {
36 | return;
37 | }
38 | setDialogButtons(
39 | getDialogButtons(instance, {
40 | closeDialog: (resolution: any) => {
41 | resolve(resolution);
42 | setOpen(false);
43 | clearUp();
44 | },
45 | })
46 | );
47 | },
48 | });
49 | return (
50 |
51 |
52 | {DialogClass.DialogTitle || "Dialog"}
53 |
54 | {dialogInstance}
55 | {dialogButtons}
56 |
57 | );
58 | };
59 |
60 | ReactDOM.render( , dialogRoot);
61 |
62 | clearUp = () => {
63 | setTimeout(() => {
64 | ReactDOM.unmountComponentAtNode(dialogRoot);
65 | try {
66 | document.body.removeChild(dialogRoot);
67 | } catch (e) {
68 | console.warn(e);
69 | }
70 | }, 1000);
71 | };
72 | });
73 | }
74 |
75 | export default showDialog;
76 |
--------------------------------------------------------------------------------
/src/Components/MarkdownRender/MarkdownRender.scss:
--------------------------------------------------------------------------------
1 | @import "~bulma/bulma";
2 |
3 | $color-primary: orange !default;
4 |
5 | .markdown-render-root {
6 | text-align: left;
7 | padding: 20px;
8 | line-height: 1.4;
9 |
10 | .markdown-title {
11 | max-width: 760px;
12 | margin: 10px auto 20px;
13 |
14 | .title {
15 | font-size: 32px;
16 | margin-bottom: 12px;
17 | }
18 |
19 | .author {
20 | font-size: 14px;
21 | }
22 |
23 | hr {
24 | margin-bottom: 6px;
25 | margin-top: 0;
26 | }
27 | }
28 |
29 | .content-wrapper {
30 | max-width: 760px;
31 | margin: auto;
32 | }
33 |
34 | h1 {
35 | font-size: 28px;
36 | font-weight: 600;
37 | margin: 12px 0 10px 0;
38 | }
39 |
40 | h2 {
41 | font-size: 26px;
42 | font-weight: 500;
43 | margin: 12px 0 6px 0;
44 | }
45 |
46 | h3 {
47 | font-size: 23px;
48 | font-weight: 500;
49 | margin: 12px 0 3px 0;
50 | }
51 |
52 | h3 {
53 | font-size: 20px;
54 | font-weight: 500;
55 | margin: 12px 0 3px 0;
56 | }
57 |
58 | h4 {
59 | font-size: 17px;
60 | font-weight: 500;
61 | margin: 12px 0 3px 0;
62 | }
63 |
64 | pre {
65 | background: none;
66 | margin: 14px 0;
67 | padding: 12px !important;
68 |
69 | code {
70 | font-family: monospace;
71 | }
72 | }
73 |
74 | li {
75 | margin-left: 1.5em;
76 | }
77 |
78 | ul,
79 | ol {
80 | margin-top: 6px;
81 | margin-bottom: 10px;
82 | }
83 |
84 | .markdown-img {
85 | margin-bottom: 12px;
86 | }
87 |
88 | .content-highlight {
89 | text-decoration: underline;
90 | text-decoration-color: orange;
91 | cursor: pointer;
92 |
93 | &.has-annotation {
94 | background: #ffa50033;
95 | }
96 | }
97 |
98 | .loading-bar {
99 | position: fixed;
100 | top: 0;
101 | left: 0;
102 | width: 100%;
103 | z-index: 1000;
104 |
105 | .fill {
106 | height: 3px;
107 | background: $color-primary;
108 | box-shadow: $color-primary 0px 0px 10px, $color-primary 0px 0px 10px;
109 | transition: all 0.2s;
110 | }
111 | }
112 |
113 | h1 {
114 | color: $color-primary;
115 | }
116 |
117 | strong {
118 | color: $color-primary;
119 | }
120 |
121 | a {
122 | color: $color-primary;
123 |
124 | svg {
125 | width: 14px;
126 | height: 14px;
127 | fill: $color-primary;
128 | margin: -2px 0 -2px 2px;
129 | }
130 |
131 | &:visited {
132 | color: adjust-color($color-primary, $lightness: -10%);
133 | }
134 | }
135 | }
136 |
137 | .highlight-button-wrapper {
138 | position: absolute;
139 | text-align: center;
140 | z-index: 10;
141 |
142 | .highlight-buttons {
143 | display: flex;
144 | margin: auto;
145 | width: 300px;
146 | font-weight: 600;
147 | //background: adjust-color($color-primary, $lightness: +40%, $saturation: +10%);
148 | cursor: pointer;
149 | box-shadow: #aaaaaa99 0px 0px 3px;
150 | border: 1px solid #aaaaaa55;
151 |
152 | .highlight-button {
153 | user-select: none;
154 | position: relative;
155 |
156 | svg {
157 | //position: absolute;
158 | //left: 20px;
159 | //top: 7px;
160 | margin-right: 10px;
161 | }
162 |
163 | backdrop-filter: blur(2px);
164 | padding: 4px;
165 |
166 | flex-grow: 1;
167 | background: #f9f9f9cc;
168 | border-right: 1px solid #aaaaaa88;
169 |
170 | &:last-of-type {
171 | border-right: none;
172 | }
173 |
174 | transition: background 0.2s;
175 |
176 | &:hover {
177 | background: #fcefd9dd;
178 | }
179 |
180 | &.del-button {
181 | &:hover {
182 | background: #fcd9dedd;
183 | }
184 | }
185 | }
186 | }
187 | }
188 |
189 | .annotation-root {
190 | textarea {
191 | border: none;
192 | font-size: 16px;
193 | line-height: 1.4;
194 | padding: 10px;
195 | min-width: 300px;
196 | min-height: 80px;
197 | max-width: 500px;
198 | max-height: 200px;
199 | }
200 | }
201 |
202 | .inline-svg {
203 | display: inline-block;
204 | width: 16px;
205 | height: 16px;
206 | margin: -2px;
207 | }
208 |
--------------------------------------------------------------------------------
/src/test.md:
--------------------------------------------------------------------------------
1 | # Promethides lupum radicis pectora stagnata lingua
2 |
3 | | Tables | Are | Cool |
4 | | ------------- | :-----------: | -----: |
5 | | col 3 is | right-aligned | \$1600 |
6 | | col 2 is | centered | \$12 |
7 | | zebra stripes | are neat | \$1 |
8 |
9 | # title 1
10 |
11 | text
12 |
13 | ## title 2
14 |
15 | text
16 |
17 | ### title 3
18 |
19 | text
20 |
21 | #### title 4
22 |
23 | text
24 |
25 | ## Fata exceptas
26 |
27 | Lorem markdownum asdqwe fdsg integer \sqrt{x^2+y^2} . Et fatis, Nabataeus illo [curvis
28 | quos](http://www.altera-praecorrupta.net/tamen) menti, quam iussi, superator
29 | cupiam sibi nostra. Flector omnia verticis et Iovis desiluit inopem huius;
30 |
31 | Lorem markdownum asdqwe fdsg integer \sqrt{x^2+y^2} . Et fatis, Nabataeus illo [curvis
32 | quos](http://www.altera-praecorrupta.net/tamen) menti, quam iussi, superator
33 | cupiam sibi nostra. Flector omnia verticis et Iovis desiluit inopem huius;
34 |
35 | Lorem markdownum asdqwe fdsg integer \sqrt{x^2+y^2} . Et fatis, Nabataeus illo [curvis
36 | quos](http://www.altera-praecorrupta.net/tamen) menti, quam iussi, superator
37 | cupiam sibi nostra. Flector omnia verticis et Iovis desiluit inopem huius;
38 |
39 | ```math
40 | \displaystyle \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
41 | ```
42 |
43 | patris quem: uvae est en sol. Corpora simul, et limine lacrimae letale Circes,
44 | licet saeva praemia cingentibus, has suscitat fiducia et a`a` b`b` ramis morata. Aquas
45 | ultra pictas tenent ab atque **et et**, sancta.
46 |
47 | ```
48 | {
49 | "a": 1
50 | }
51 | ```
52 |
53 | ```json
54 | {
55 | "a": 1
56 | }
57 | ```
58 |
59 | ```javascript
60 | if (8 != pixelMinisiteListserv) {
61 | im.kilohertz(ioPeripheral, memoryBatch, cdn);
62 | wi_parse = baseband;
63 | bareFloatingSmm(fios);
64 | } else {
65 | expansion = data(cut - 64, 2 + sata);
66 | pingTftp.port = log;
67 | vertical.abendVrml(5);
68 | }
69 | compression *= office.device(4, parallelMetafile, storageCardBoot);
70 | if (zoneAlphaDesign(kvm, vdu, web) - southbridge) {
71 | microcomputer_computing_map += pramSpeed(wddm + iscsi);
72 | jsfJumper.ssl_page -= phishing;
73 | joystick_plagiarism += ipad(parameter, 53);
74 | } else {
75 | threading_traceroute = 1;
76 | syn(lpi, pmu);
77 | wais_hard.repeaterIpPda -= bingUrlReadme;
78 | }
79 | ```
80 |
81 | 
82 |
83 | Genus _sit_ movit tulerunt fortius qui, in imagine igitur concussit, est
84 | fluitantia findit; precor sed. Reliquit perforat Tartara, inter caput facitis
85 | Cadmeida et [taurus pharetratus danda](http://www.auspicio.io/etastra.html)
86 | penates, manente. Ventis iam enim onerosa, nisi, dea viri prius volitat: capilli
87 | aevum cum terrae, imagine? In gloria gentis territa itque: arboreis dicenda,
88 | omnia baculi, resupinus tenentem. Medusaeo genus; Aiax in vocant quaeque
89 | erroribus futura, Haemonio inquit; alte.
90 |
91 | ## An tum
92 |
93 |
94 | Hello ` abc `
95 |
96 | Scopulum **pavens** seductus, prensantem surgere: dubites deus pectora? Pone
97 | tria quae foret pervenit pulsat: origine: ipsorum, aut!
98 | [Ululatibus](http://www.habebat.io/tibi-timeam.php) vidisset parte nuribus:
99 | [Pirithoum](http://www.profanatlapitharum.org/in-dixit) nata _cladis ipse enim_
100 | gerentes potest sumat iam; verba iubar fisso invitumque. Pendeat libatos et
101 | mille tibi movebere quod mox figuram [si](http://troasque.com/)?
102 |
103 | 1. Et digitis modo minores portas aequatam
104 | 2. Quae nam Phoebus metues
105 | 3. Spiritus figuras
106 | 4. Et virgo
107 | 5. Nostris agros est
108 |
109 | Venus **dat** aut exsecratur moveat facit ipse igne, nostras quid tenet, est nec
110 | sum. Albis laborum, per perit superata, scissae magis, loci nec placidi
111 | experiensque voluit [Diomedeos](http://quodtumidus.com/) summa: aeolis qua. Et
112 | superare tamen. Plurima in simul cornibus magni muta illac praestem querella
113 | accessus candidus.
114 |
115 | Ecce denegat quantus. Et adest priorum de suae animosus alba subduxit _alternae
116 | tetenderat_ deos quid ferrumque sumptaque constituit spargimur. Oleaster
117 | pignora, Cephalus vibrant; silet ante quadrupes Cyllenius portus Styga sepulcro
118 | subdit dissimulator forte Iovis indigestaque turba pinus.
119 |
--------------------------------------------------------------------------------
/public/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | iframe
8 |
9 |
10 |
11 |
12 | this is test iframe page
13 |
14 |
Promethides lupum radicis pectora stagnata lingua
15 |
16 |
17 |
18 | Tables
19 | Are
20 | Cool
21 |
22 |
23 |
24 |
25 | col 3 is
26 | right-aligned
27 | $1600
28 |
29 |
30 | col 2 is
31 | centered
33 |
34 | $12
35 |
36 |
37 | zebra stripes
38 | are
40 | neat
41 | $1
42 |
43 |
44 |
45 |
title 1
46 |
text
47 |
title 2
48 |
text
49 |
title 3
50 |
text
51 |
title 4
52 |
text
53 |
Fata exceptas
54 |
Lorem markdownum asdqwe fdsg integer . Et
55 | fatis, Nabataeus illo curvis
56 | quos menti, quam iussi, superator
57 | cupiam sibi nostra. Flector omnia verticis et Iovis desiluit inopem huius;
58 |
Lorem markdownum asdqwe fdsg integer . Et
59 | fatis, Nabataeus illo curvis
60 | quos menti, quam iussi, superator
61 | cupiam sibi nostra. Flector omnia verticis et Iovis desiluit inopem huius;
62 |
Lorem markdownum asdqwe fdsg integer . Et
63 | fatis, Nabataeus illo curvis
64 | quos menti, quam iussi, superator
65 | cupiam sibi nostra. Flector omnia verticis et Iovis desiluit inopem huius;
66 |
patris quem: uvae est en sol. Corpora simul, et limine lacrimae letale Circes,
67 | licet saeva praemia cingentibus, has suscitat fiducia et aa bb ramis morata. Aquas
68 | ultra pictas tenent ab atque et et , sancta.
69 |
{
70 | "a" : 1
71 | }
72 |
73 |
{
74 | "a" : 1
75 | }
76 |
77 |
if ( 8 != pixelMinisiteListserv) {
78 | im.kilohertz(ioPeripheral, memoryBatch, cdn);
79 | wi_parse = baseband;
80 | bareFloatingSmm(fios);
81 | } else {
82 | expansion = data(cut - 64 , 2 + sata);
83 | pingTftp.port = log;
84 | vertical.abendVrml( 5 );
85 | }
86 | compression *= office.device( 4 , parallelMetafile, storageCardBoot);
87 | if (zoneAlphaDesign(kvm, vdu, web) - southbridge) {
88 | microcomputer_computing_map += pramSpeed(wddm + iscsi);
89 | jsfJumper.ssl_page -= phishing;
90 | joystick_plagiarism += ipad(parameter, 53 );
91 | } else {
92 | threading_traceroute = 1 ;
93 | syn(lpi, pmu);
94 | wais_hard.repeaterIpPda -= bingUrlReadme;
95 | }
96 |
97 |
98 |
Genus sit movit tulerunt fortius qui, in imagine igitur concussit, est
99 | fluitantia findit; precor sed. Reliquit perforat Tartara, inter caput facitis
100 | Cadmeida et taurus pharetratus danda
101 | penates, manente. Ventis iam enim onerosa, nisi, dea viri prius volitat: capilli
102 | aevum cum terrae, imagine? In gloria gentis territa itque: arboreis dicenda,
103 | omnia baculi, resupinus tenentem. Medusaeo genus; Aiax in vocant quaeque
104 | erroribus futura, Haemonio inquit; alte.
105 |
An tum
106 |
Hello <code> abc </code>
107 |
Scopulum pavens seductus, prensantem surgere: dubites deus pectora? Pone
108 | tria quae foret pervenit pulsat: origine: ipsorum, aut!
109 | Ululatibus vidisset parte nuribus:
110 | Pirithoum nata cladis ipse enim
111 | gerentes potest sumat iam; verba iubar fisso invitumque. Pendeat libatos et
112 | mille tibi movebere quod mox figuram si ?
113 |
114 |
115 | Et digitis modo minores portas aequatam
116 | Quae nam Phoebus metues
117 | Spiritus figuras
118 | Et virgo
119 | Nostris agros est
120 |
121 |
Venus dat aut exsecratur moveat facit ipse igne, nostras quid tenet, est nec
122 | sum. Albis laborum, per perit superata, scissae magis, loci nec placidi
123 | experiensque voluit Diomedeos summa: aeolis qua. Et
124 | superare tamen. Plurima in simul cornibus magni muta illac praestem querella
125 | accessus candidus.
126 |
Ecce denegat quantus. Et adest priorum de suae animosus alba subduxit _alternae
127 | tetenderat_ deos quid ferrumque sumptaque constituit spargimur. Oleaster
128 | pignora, Cephalus vibrant; silet ante quadrupes Cyllenius portus Styga sepulcro
129 | subdit dissimulator forte Iovis indigestaque turba pinus.
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/design/logo/FullColor_1280x1024.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Created with Fabric.js 3.6.3
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Created with Fabric.js 3.6.3
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/design/logo/FullColor_1280x1024.eps:
--------------------------------------------------------------------------------
1 | %!PS-Adobe-3.0 EPSF-3.0
2 | %%Creator: cairo 1.16.0 (https://cairographics.org)
3 | %%CreationDate: Tue Nov 3 10:50:40 2020
4 | %%Pages: 1
5 | %%DocumentData: Clean7Bit
6 | %%LanguageLevel: 3
7 | %%BoundingBox: 0 0 961 769
8 | %%EndComments
9 | %%BeginProlog
10 | 50 dict begin
11 | /q { gsave } bind def
12 | /Q { grestore } bind def
13 | /cm { 6 array astore concat } bind def
14 | /w { setlinewidth } bind def
15 | /J { setlinecap } bind def
16 | /j { setlinejoin } bind def
17 | /M { setmiterlimit } bind def
18 | /d { setdash } bind def
19 | /m { moveto } bind def
20 | /l { lineto } bind def
21 | /c { curveto } bind def
22 | /h { closepath } bind def
23 | /re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
24 | 0 exch rlineto 0 rlineto closepath } bind def
25 | /S { stroke } bind def
26 | /f { fill } bind def
27 | /f* { eofill } bind def
28 | /n { newpath } bind def
29 | /W { clip } bind def
30 | /W* { eoclip } bind def
31 | /BT { } bind def
32 | /ET { } bind def
33 | /BDC { mark 3 1 roll /BDC pdfmark } bind def
34 | /EMC { mark /EMC pdfmark } bind def
35 | /cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
36 | /Tj { show currentpoint cairo_store_point } bind def
37 | /TJ {
38 | {
39 | dup
40 | type /stringtype eq
41 | { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
42 | } forall
43 | currentpoint cairo_store_point
44 | } bind def
45 | /cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
46 | cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
47 | /Tf { pop /cairo_font exch def /cairo_font_matrix where
48 | { pop cairo_selectfont } if } bind def
49 | /Td { matrix translate cairo_font_matrix matrix concatmatrix dup
50 | /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
51 | /cairo_font where { pop cairo_selectfont } if } bind def
52 | /Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
53 | cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
54 | /g { setgray } bind def
55 | /rg { setrgbcolor } bind def
56 | /d1 { setcachedevice } bind def
57 | /cairo_data_source {
58 | CairoDataIndex CairoData length lt
59 | { CairoData CairoDataIndex get /CairoDataIndex CairoDataIndex 1 add def }
60 | { () } ifelse
61 | } def
62 | /cairo_flush_ascii85_file { cairo_ascii85_file status { cairo_ascii85_file flushfile } if } def
63 | /cairo_image { image cairo_flush_ascii85_file } def
64 | /cairo_imagemask { imagemask cairo_flush_ascii85_file } def
65 | %%EndProlog
66 | %%BeginSetup
67 | %%EndSetup
68 | %%Page: 1 1
69 | %%BeginPageSetup
70 | %%PageBoundingBox: 0 0 961 769
71 | %%EndPageSetup
72 | q 0 0 961 769 rectclip
73 | 1 0 0 -1 0 769 cm q
74 | 1 g
75 | 0 0 960.023 768.02 re f
76 | 0.964706 0.721569 0.0431373 rg
77 | 247.145 401.801 m 346.629 369.957 l 404.332 142.211 l 304.844 174.047 l
78 | 247.145 401.801 l f
79 | 0.352941 0.317647 0.67451 rg
80 | 530.52 139.84 m 629.996 107.996 l 712.867 353.129 l 615.531 393.867 l 530.52
81 | 139.84 l f
82 | 0.945098 0.933333 0.101961 rg
83 | 362.238 308.852 m 404.449 142.254 l 540.238 461.723 l 440.75 493.551 l
84 | 362.238 308.852 l f
85 | 0.929412 0.666667 0.0588235 rg
86 | 404.359 142.168 m 304.867 174.004 l 362.148 308.766 l h
87 | 404.359 142.168 m f
88 | 0.317647 0.494118 0.788235 rg
89 | 440.828 493.535 m 540.316 461.707 l 629.914 108.039 l 530.438 139.883 l
90 | 440.828 493.535 l f
91 | 0.364706 0.772549 0.521569 rg
92 | 440.789 493.637 m 540.277 461.809 l 482.996 327.047 l h
93 | 440.789 493.637 m f
94 | 0.172549 0.215686 0.54902 rg
95 | 629.957 107.949 m 530.477 139.793 l 582.5 295.258 l h
96 | 629.957 107.949 m f
97 | q
98 | 302.145 590.699 m 293.711 590.699 l 288.809 563.332 l 283.562 590.699 l
99 | 275.336 590.699 l 268.602 553.336 l 274.629 553.336 l 279.457 587.008 l
100 | 285.41 558.09 l 292.426 558.09 l 298.023 587.008 l 302.852 553.336 l 308.598
101 | 553.336 l h
102 | 321.715 574.113 m 321.852 578.223 322.926 581.316 324.934 583.406 c 326.941
103 | 585.484 329.484 586.52 332.555 586.52 c 334.395 586.52 336.086 586.25 337.621
104 | 585.711 c 339.156 585.156 340.801 584.316 342.555 583.184 c 345.316 587.156
105 | l 343.562 588.523 341.562 589.59 339.32 590.348 c 337.074 591.105 334.82
106 | 591.484 332.555 591.484 c 327.168 591.484 322.973 589.723 319.973 586.195
107 | c 316.969 582.68 315.465 577.969 315.465 572.062 c 315.465 568.32 316.133
108 | 564.973 317.461 562.02 c 318.781 559.066 320.672 556.75 323.133 555.078
109 | c 325.582 553.402 328.438 552.566 331.699 552.566 c 336.426 552.566 340.148
110 | 554.207 342.863 557.484 c 345.582 560.773 346.941 565.254 346.941 570.926
111 | c 346.941 572.008 346.895 573.07 346.809 574.113 c h
112 | 331.773 557.453 m 328.887 557.453 326.547 558.484 324.758 560.543 c 322.965
113 | 562.59 321.949 565.602 321.715 569.582 c 341.137 569.582 l 341.086 565.652
114 | 340.234 562.648 338.582 560.57 c 336.926 558.492 334.656 557.453 331.773
115 | 557.453 c h
116 | 365.445 558.238 m 368.332 554.457 371.902 552.566 376.156 552.566 c 380.93
117 | 552.566 384.449 554.266 386.715 557.66 c 388.988 561.07 390.125 565.844
118 | 390.125 571.988 c 390.125 577.848 388.828 582.559 386.227 586.121 c 383.629
119 | 589.695 379.984 591.484 375.297 591.484 c 371.004 591.484 367.629 589.922
120 | 365.168 586.801 c 364.738 590.699 l 359.496 590.699 l 359.496 538.312 l
121 | 365.445 537.605 l h
122 | 373.953 586.652 m 377.074 586.652 379.473 585.43 381.148 582.977 c 382.832
123 | 580.516 383.672 576.852 383.672 571.988 c 383.672 567.164 382.93 563.523
124 | 381.441 561.074 c 379.945 558.613 377.734 557.383 374.812 557.383 c 372.922
125 | 557.383 371.184 557.945 369.598 559.078 c 368.012 560.211 366.629 561.629
126 | 365.445 563.332 c 365.445 581.84 l 366.441 583.355 367.672 584.539 369.141
127 | 585.383 c 370.605 586.23 372.211 586.652 373.953 586.652 c h
128 | 403.805 571.059 m 403.805 565.961 l 428.617 565.961 l 428.617 571.059 l
129 | h
130 | 469.172 552.566 m 471.301 552.566 473.035 553.309 474.371 554.797 c 475.723
131 | 556.281 476.395 558.984 476.395 562.906 c 476.395 590.699 l 470.945 590.699
132 | l 470.945 563.91 l 470.945 561.309 470.777 559.547 470.445 558.621 c 470.109
133 | 557.695 469.305 557.234 468.035 557.234 c 465.625 557.234 463.449 558.727
134 | 461.508 561.707 c 461.508 590.699 l 455.984 590.699 l 455.984 563.91 l
135 | 455.984 561.309 455.816 559.547 455.48 558.621 c 455.156 557.695 454.355
136 | 557.234 453.074 557.234 c 450.664 557.234 448.488 558.727 446.547 561.707
137 | c 446.547 590.699 l 441.098 590.699 l 441.098 553.336 l 445.703 553.336
138 | l 446.133 557.734 l 447.266 556.082 448.457 554.805 449.707 553.91 c 450.957
139 | 553.016 452.461 552.566 454.211 552.566 c 457.707 552.566 459.953 554.219
140 | 460.945 557.527 c 462.078 555.914 463.285 554.684 464.566 553.836 c 465.836
141 | 552.988 467.371 552.566 469.172 552.566 c h
142 | 513.895 582.547 m 513.895 584.016 514.133 585.09 514.605 585.77 c 515.078
143 | 586.457 515.859 586.969 516.953 587.305 c 515.461 591.559 l 511.816 591.086
144 | 509.547 589.312 508.652 586.242 c 507.332 587.934 505.668 589.234 503.66
145 | 590.141 c 501.652 591.035 499.418 591.484 496.957 591.484 c 493.223 591.484
146 | 490.285 590.445 488.137 588.367 c 485.98 586.281 484.902 583.488 484.902
147 | 579.992 c 484.902 576.164 486.406 573.211 489.406 571.133 c 492.41 569.055
148 | 496.723 568.016 502.348 568.016 c 507.871 568.016 l 507.871 564.973 l 507.871
149 | 562.324 507.117 560.418 505.609 559.258 c 504.094 558.105 501.895 557.527
150 | 499.008 557.527 c 496.172 557.527 492.938 558.164 489.305 559.434 c 487.664
151 | 554.898 l 492.066 553.344 496.156 552.566 499.938 552.566 c 504.477 552.566
152 | 507.938 553.629 510.32 555.758 c 512.703 557.883 513.895 560.836 513.895
153 | 564.617 c h
154 | 498.445 587.008 m 500.289 587.008 502.051 586.535 503.734 585.59 c 505.41
155 | 584.645 506.785 583.348 507.871 581.691 c 507.871 572.121 l 502.492 572.121
156 | l 498.516 572.121 495.656 572.785 493.914 574.113 c 492.16 575.434 491.285
157 | 577.371 491.285 579.918 c 491.285 584.645 493.672 587.008 498.445 587.008
158 | c h
159 | 554.883 552.566 m 556.387 552.566 558.137 552.82 560.125 553.336 c 559.27
160 | 566.035 l 554.453 566.035 l 554.453 558.016 l 554.098 558.016 l 548.328
161 | 558.016 544.242 562.125 541.84 570.348 c 541.84 586.094 l 549.418 586.094
162 | l 549.418 590.699 l 529.996 590.699 l 529.996 586.094 l 535.871 586.094
163 | l 535.871 557.941 l 529.996 557.941 l 529.996 553.336 l 540.348 553.336
164 | l 541.484 562.195 l 542.992 558.984 544.797 556.578 546.906 554.973 c 549.004
165 | 553.367 551.66 552.566 554.883 552.566 c h
166 | 578.559 537.605 m 578.559 590.699 l 572.605 590.699 l 572.605 538.312 l
167 | h
168 | 603.445 553.336 m 586.711 570.215 l 605.422 590.699 l 597.551 590.699 l
169 | 579.191 570.289 l 595.777 553.336 l h
170 | 619.469 574.113 m 619.605 578.223 620.68 581.316 622.688 583.406 c 624.699
171 | 585.484 627.238 586.52 630.309 586.52 c 632.152 586.52 633.84 586.25 635.375
172 | 585.711 c 636.91 585.156 638.555 584.316 640.309 583.184 c 643.07 587.156
173 | l 641.316 588.523 639.32 589.59 637.074 590.348 c 634.828 591.105 632.574
174 | 591.484 630.309 591.484 c 624.922 591.484 620.73 589.723 617.727 586.195
175 | c 614.723 582.68 613.223 577.969 613.223 572.062 c 613.223 568.32 613.887
176 | 564.973 615.215 562.02 c 616.535 559.066 618.426 556.75 620.887 555.078
177 | c 623.34 553.402 626.195 552.566 629.453 552.566 c 634.18 552.566 637.902
178 | 554.207 640.617 557.484 c 643.336 560.773 644.695 565.254 644.695 570.926
179 | c 644.695 572.008 644.652 573.07 644.562 574.113 c h
180 | 629.527 557.453 m 626.641 557.453 624.305 558.484 622.512 560.543 c 620.719
181 | 562.59 619.707 565.602 619.469 569.582 c 638.891 569.582 l 638.84 565.652
182 | 637.988 562.648 636.336 560.57 c 634.68 558.492 632.41 557.453 629.527
183 | 557.453 c h
184 | 682.492 552.566 m 683.996 552.566 685.746 552.82 687.734 553.336 c 686.879
185 | 566.035 l 682.062 566.035 l 682.062 558.016 l 681.707 558.016 l 675.938
186 | 558.016 671.852 562.125 669.449 570.348 c 669.449 586.094 l 677.027 586.094
187 | l 677.027 590.699 l 657.605 590.699 l 657.605 586.094 l 663.48 586.094
188 | l 663.48 557.941 l 657.605 557.941 l 657.605 553.336 l 667.957 553.336 l
189 | 669.094 562.195 l 670.602 558.984 672.406 556.578 674.516 554.973 c 676.613
190 | 553.367 679.27 552.566 682.492 552.566 c h
191 | 682.492 552.566 m W n
192 | [1.476959 0 0 1.476959 268.602631 537.604497] concat
193 | /CairoFunction
194 | << /FunctionType 2
195 | /Domain [ 0 1 ]
196 | /C0 [ 0.972549 0.301961 0.619608 ]
197 | /C1 [ 1 0.454902 0.458824 ]
198 | /N 1
199 | >>
200 | def
201 | << /ShadingType 2
202 | /ColorSpace /DeviceRGB
203 | /Coords [ 0 0 0 54 ]
204 | /Extend [ true true ]
205 | /Function CairoFunction
206 | >>
207 | shfill
208 | Q
209 | Q Q
210 | showpage
211 | %%Trailer
212 | end
213 | %%EOF
214 |
--------------------------------------------------------------------------------
/src/Components/MarkdownRender/IframeRender.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./MarkdownRender.scss";
4 | import TextareaAutosize from "@material-ui/core/TextareaAutosize";
5 | import Button from "@material-ui/core/Button";
6 | import Marker from "../../lib/classes/Marker";
7 | import showDialog from "../../utils/showDialog";
8 | import makeid from "../../utils/makeid";
9 |
10 | const demoMultiRangeMode = window.location.hash.indexOf("multi-range") >= 0;
11 |
12 | const LOCALSTORAGE_HIGHLIGHTS_KEY = "web-marker-highlights-iframe";
13 |
14 | class AnnotationDialog extends React.Component {
15 | static DialogTitle = "Annotation";
16 | state = { text: this.props.text || "" };
17 |
18 | DialogButtons = ({ closeDialog }) => [
19 | closeDialog(this.state.text)}
21 | variant="contained"
22 | key={"ok"}
23 | >
24 | OK
25 | ,
26 | ];
27 |
28 | render() {
29 | return (
30 |
31 | {
35 | this.setState({ text: e.target.value });
36 | }}
37 | />
38 |
39 | );
40 | }
41 | }
42 |
43 | function findProperTopLeftAndWidth(clientRect) {
44 | if (!clientRect) {
45 | return [0, 0];
46 | }
47 | let top = clientRect.top - 35;
48 | if (top - window.scrollY < 0) {
49 | top = window.scrollY;
50 | }
51 | let width = Math.min(clientRect.width, window.innerWidth);
52 | width = Math.max(width, 300);
53 | let left = clientRect.left + clientRect.width / 2 - width / 2;
54 | if (left < 10) {
55 | left = 10;
56 | }
57 | if (left + width > window.innerWidth - 10) {
58 | left += window.innerWidth - 10 - (left + width);
59 | }
60 | return [top, left, width];
61 | }
62 |
63 | class IframeRender extends React.Component {
64 | highlights = {};
65 | mapHighlightIdToRange = {};
66 | selectedHighlightId = null;
67 |
68 | iframeRef = React.createRef();
69 |
70 | state = { userSelection: {}, hideHighlightButtons: true };
71 |
72 | loadHighlightsFromLocalStorage() {
73 | const data = JSON.parse(localStorage[LOCALSTORAGE_HIGHLIGHTS_KEY]);
74 | Object.keys(data).forEach((key) => {
75 | data[key].uid = key;
76 | });
77 | return data;
78 | }
79 |
80 | saveHighlightsToLocalStorage() {
81 | if (demoMultiRangeMode) {
82 | return;
83 | }
84 | const toSave = {};
85 | Object.keys(this.highlights).forEach((key) => {
86 | toSave[key] = { ...this.highlights[key], id: undefined };
87 | });
88 | localStorage[LOCALSTORAGE_HIGHLIGHTS_KEY] = JSON.stringify(toSave);
89 | }
90 |
91 |
92 |
93 | paint(serializedRange) {
94 | this.marker.paint(serializedRange);
95 | Marker.clearSelection(this.marker.window);
96 | this.highlights[serializedRange.uid] = serializedRange;
97 | this.mapHighlightIdToRange[serializedRange.uid] = this.marker.deserializeRange(
98 | serializedRange
99 | );
100 | this.saveHighlightsToLocalStorage();
101 | }
102 |
103 | setUserSelectionByRange(range) {
104 | const iframePos = this.iframeRef.current.getBoundingClientRect();
105 | let pos = range.getBoundingClientRect();
106 |
107 | let calculatedPos = {
108 | top: pos.top + window.scrollY + iframePos.top,
109 | left: pos.left + window.scrollX + iframePos.left,
110 | width: pos.width,
111 | height: pos.height,
112 | };
113 | if (calculatedPos.width === 0) {
114 | calculatedPos.width = 400;
115 | calculatedPos.top = window.pointerPos.y - 10;
116 | calculatedPos.left = window.pointerPos.x - 200;
117 | }
118 | this.setState({
119 | userSelection: {
120 | range,
121 | clientRect: calculatedPos,
122 | },
123 | hideHighlightButtons: false,
124 | });
125 | }
126 |
127 | mouseMoveListener = (e) => {
128 | const { pageX, pageY } = e;
129 | window.pointerPos = { x: pageX, y: pageY };
130 | };
131 | mouseupListener = (e) => {
132 | setTimeout(() => {
133 | this.handleMouseUp(e, false);
134 | });
135 | };
136 |
137 | handleMouseUp = (e, calledRecursively = false) => {
138 | const contentWindow = this.iframeRef.current.contentWindow;
139 | try {
140 | const selection = contentWindow.getSelection();
141 | if (!selection.toString() && !demoMultiRangeMode) {
142 | this.setState({ hideHighlightButtons: true });
143 | return;
144 | }
145 |
146 | if (!selection.rangeCount) {
147 | return null;
148 | }
149 |
150 | let range = null;
151 | try {
152 | range = selection.getRangeAt(0);
153 | } catch (e) {}
154 | if (!range) {
155 | return null;
156 | }
157 |
158 | this.selectedHighlightId = null;
159 |
160 | this.setUserSelectionByRange(range);
161 | if (demoMultiRangeMode) {
162 | const serialized = this.marker.serializeRange(
163 | this.state.userSelection.range
164 | );
165 | if (!serialized) {
166 | return;
167 | }
168 | this.paint(serialized);
169 | }
170 | } finally {
171 | if (demoMultiRangeMode) {
172 | const selection = contentWindow.getSelection();
173 | if (selection.isCollapsed && !this.isHighlightElement(e.target)) {
174 | selection.modify("move", "forward", "character");
175 | selection.modify("extend", "backward", "word");
176 | selection.modify("move", "backward", "character");
177 | selection.modify("extend", "forward", "word");
178 | if (!calledRecursively) {
179 | this.handleMouseUp(e, true);
180 | }
181 | }
182 | setTimeout(() => {
183 | this.setState({
184 | hideHighlightButtons: Object.keys(this.highlights).length === 0,
185 | });
186 | });
187 | }
188 | }
189 | };
190 |
191 | componentDidMount() {
192 | this.iframeRef.current.onload = () => {
193 | const contentWindow = this.iframeRef.current.contentWindow;
194 | this.marker = new Marker({
195 | rootElement: contentWindow.document.body,
196 | eventHandler: {
197 | onHighlightClick: (context, element) => {
198 | setTimeout(() => {
199 | if (demoMultiRangeMode) {
200 | // click again to delete in multi range mode
201 | this.marker.unpaint(this.highlights[context.serializedRange.uid]);
202 | delete this.highlights[context.serializedRange.uid];
203 | } else {
204 | this.selectedHighlightId = context.serializedRange.uid;
205 | this.setUserSelectionByRange(
206 | this.mapHighlightIdToRange[this.selectedHighlightId]
207 | );
208 | }
209 | }, 20);
210 | },
211 | onHighlightHoverStateChange: (context, element, hovering) => {
212 | if (hovering) {
213 | element.style.backgroundColor = "#EEEEEE";
214 | } else {
215 | context.marker.highlightPainter.paintHighlight(context, element);
216 | }
217 | },
218 | },
219 | highlightPainter: {
220 | paintHighlight: (context, element) => {
221 | element.style.textDecoration = "underline";
222 | element.style.textDecorationColor = "#f6b80b";
223 | if (context.serializedRange.annotation) {
224 | element.style.backgroundColor = "rgba(246,184,11, 0.3)";
225 | } else {
226 | element.style.backgroundColor = "initial";
227 | }
228 | },
229 | },
230 | });
231 |
232 | if (!demoMultiRangeMode && localStorage[LOCALSTORAGE_HIGHLIGHTS_KEY]) {
233 | this.highlights = this.loadHighlightsFromLocalStorage();
234 | Object.keys(this.highlights).forEach((id) => {
235 | this.paint(this.highlights[id]);
236 | });
237 | }
238 |
239 | contentWindow.document.addEventListener("mouseup", this.mouseupListener);
240 | contentWindow.addEventListener("pointermove", this.mouseMoveListener);
241 | this.marker.addEventListeners();
242 | }
243 | }
244 |
245 | componentWillUnmount() {
246 | const contentWindow = this.iframeRef.current.contentWindow;
247 | contentWindow.document.removeEventListener("mouseup", this.mouseupListener);
248 | contentWindow.removeEventListener("pointermove", this.mouseMoveListener);
249 | this.marker.removeEventListeners();
250 | }
251 |
252 | render() {
253 | const [top, left, width] = findProperTopLeftAndWidth(
254 | (this.state.userSelection || {}).clientRect
255 | );
256 | return (
257 |
258 | {!!this.state.userSelection &&
259 | !this.state.hideHighlightButtons &&
260 | ReactDOM.createPortal(
261 |
269 |
270 | {this.renderHighlightButtons()}
271 |
272 |
,
273 | document.body
274 | )}
275 |
276 |
277 |
278 | Here is a random number: {Math.random()}, try highlighting around
279 | it.
280 |
281 |
282 |
abc
283 |
284 | blackli sted
285 |
286 |
vcxdsf
287 |
qwerthgf
288 |
289 |
290 | 000000000000000000000000000000000000000000000000
291 |
292 | visibility hidden !! {" "}
293 | test
294 |
295 | 111111111111111111111111111111111111111111111111
296 |
297 |
298 |
299 |
300 | );
301 | }
302 |
303 | renderHighlightButtons() {
304 | if (demoMultiRangeMode) {
305 | return this.renderHighlightButtonsMultiRangeMode();
306 | } else {
307 | return this.renderHighlightButtonsDefault();
308 | }
309 | }
310 |
311 | renderHighlightButtonsDefault() {
312 | const doHighlight = () => {
313 | const serialized = this.marker.serializeRange(
314 | this.state.userSelection.range
315 | );
316 | if (!serialized) {
317 | return;
318 | }
319 |
320 | this.paint(serialized);
321 |
322 | this.setState({ hideHighlightButtons: true });
323 | return { uid: serialized.uid };
324 | };
325 |
326 | const doDelete = () => {
327 | if (this.highlights[this.selectedHighlightId].annotation) {
328 | // eslint-disable-next-line no-restricted-globals
329 | if (!confirm("Are you sure? The annotation will be removed as well")) {
330 | return;
331 | }
332 | }
333 | this.marker.unpaint(this.highlights[this.selectedHighlightId]);
334 | delete this.highlights[this.selectedHighlightId];
335 | this.saveHighlightsToLocalStorage();
336 | this.setState({ hideHighlightButtons: true });
337 | };
338 |
339 | const doAnnotate = () => {
340 | if (!this.selectedHighlightId) {
341 | const result = doHighlight();
342 | this.selectedHighlightId = result.uid;
343 | }
344 | let selectedHighlightId = this.selectedHighlightId;
345 | showDialog(AnnotationDialog, {
346 | text: this.highlights[selectedHighlightId].annotation || "",
347 | }).then((data) => {
348 | this.highlights[selectedHighlightId].annotation = data;
349 | this.saveHighlightsToLocalStorage();
350 | this.marker.paintHighlights(selectedHighlightId);
351 | });
352 | };
353 |
354 | return (
355 | <>
356 | {!this.selectedHighlightId && (
357 |
358 | Highlight
359 |
360 | )}
361 | {!!this.selectedHighlightId && (
362 |
363 | Delete
364 |
365 | )}
366 |
367 | Annotate
368 |
369 | >
370 | );
371 | }
372 |
373 | renderHighlightButtonsMultiRangeMode() {
374 | const onSubmit = () => {
375 | const data = { id: makeid(), ranges: [] };
376 | Object.keys(this.highlights).forEach((key) => {
377 | data.ranges.push(this.highlights[key]);
378 | });
379 | alert("selection created: " + JSON.stringify(data, null, 4));
380 | onDiscard();
381 | };
382 | const onDiscard = () => {
383 | Object.keys(this.highlights).forEach((key) => {
384 | this.marker.unpaint(this.highlights[key]);
385 | delete this.highlights[key];
386 | });
387 | this.setState({
388 | hideHighlightButtons: true,
389 | });
390 | };
391 | return (
392 | <>
393 |
394 | Submit
395 |
396 |
397 | Discard
398 |
399 | >
400 | );
401 | }
402 |
403 | isHighlightElement(element) {
404 | try {
405 | return element.tagName === "HIGHLIGHT";
406 | } catch {
407 | return false;
408 | }
409 | }
410 | }
411 |
412 | export default IframeRender;
413 |
--------------------------------------------------------------------------------
/src/Components/MarkdownRender/MarkdownRender.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import Markdown from "markdown-to-jsx";
4 | import makeMarkdownOverrides from "./overrides";
5 | import "./MarkdownRender.scss";
6 | import TextareaAutosize from "@material-ui/core/TextareaAutosize";
7 | import Button from "@material-ui/core/Button";
8 | import { parseMarkdown } from "../../utils/markdown";
9 | import Marker from "../../lib/classes/Marker";
10 | import showDialog from "../../utils/showDialog";
11 | import makeid from "../../utils/makeid";
12 |
13 | const demoMultiRangeMode = window.location.hash.indexOf("multi-range") >= 0;
14 |
15 | const LOCALSTORAGE_HIGHLIGHTS_KEY = "web-marker-highlights";
16 |
17 | class AnnotationDialog extends React.Component {
18 | static DialogTitle = "Annotation";
19 | state = { text: this.props.text || "" };
20 |
21 | DialogButtons = ({ closeDialog }) => [
22 | closeDialog(this.state.text)}
24 | variant="contained"
25 | key={"ok"}
26 | >
27 | OK
28 | ,
29 | ];
30 |
31 | render() {
32 | return (
33 |
34 | {
38 | this.setState({ text: e.target.value });
39 | }}
40 | />
41 |
42 | );
43 | }
44 | }
45 |
46 | function findProperTopLeftAndWidth(clientRect) {
47 | if (!clientRect) {
48 | return [0, 0];
49 | }
50 | let top = clientRect.top - 35;
51 | if (top - window.scrollY < 0) {
52 | top = window.scrollY;
53 | }
54 | let width = Math.min(clientRect.width, window.innerWidth);
55 | width = Math.max(width, 300);
56 | let left = clientRect.left + clientRect.width / 2 - width / 2;
57 | if (left < 10) {
58 | left = 10;
59 | }
60 | if (left + width > window.innerWidth - 10) {
61 | left += window.innerWidth - 10 - (left + width);
62 | }
63 | return [top, left, width];
64 | }
65 |
66 | class MarkdownRender extends React.Component {
67 | highlights = {};
68 | mapHighlightIdToRange = {};
69 | selectedHighlightId = null;
70 |
71 | state = { userSelection: {}, hideHighlightButtons: true };
72 |
73 | loadHighlightsFromLocalStorage() {
74 | const data = JSON.parse(localStorage[LOCALSTORAGE_HIGHLIGHTS_KEY]);
75 | Object.keys(data).forEach((key) => {
76 | data[key].uid = key;
77 | });
78 | return data;
79 | }
80 |
81 | saveHighlightsToLocalStorage() {
82 | if (demoMultiRangeMode) {
83 | return;
84 | }
85 | const toSave = {};
86 | Object.keys(this.highlights).forEach((key) => {
87 | toSave[key] = { ...this.highlights[key], id: undefined };
88 | });
89 | localStorage[LOCALSTORAGE_HIGHLIGHTS_KEY] = JSON.stringify(toSave);
90 | }
91 |
92 | marker = new Marker({
93 | rootElement: document.body,
94 | eventHandler: {
95 | onHighlightClick: (context, element) => {
96 | setTimeout(() => {
97 | if (demoMultiRangeMode) {
98 | // click again to delete in multi range mode
99 | this.marker.unpaint(this.highlights[context.serializedRange.uid]);
100 | delete this.highlights[context.serializedRange.uid];
101 | } else {
102 | this.selectedHighlightId = context.serializedRange.uid;
103 | this.setUserSelectionByRange(
104 | this.mapHighlightIdToRange[this.selectedHighlightId]
105 | );
106 | }
107 | }, 20);
108 | },
109 | onHighlightHoverStateChange: (context, element, hovering) => {
110 | if (hovering) {
111 | element.style.backgroundColor = "#EEEEEE";
112 | } else {
113 | context.marker.highlightPainter.paintHighlight(context, element);
114 | }
115 | },
116 | },
117 | highlightPainter: {
118 | paintHighlight: (context, element) => {
119 | element.style.textDecoration = "underline";
120 | element.style.textDecorationColor = "#f6b80b";
121 | if (context.serializedRange.annotation) {
122 | element.style.backgroundColor = "rgba(246,184,11, 0.3)";
123 | } else {
124 | element.style.backgroundColor = "initial";
125 | }
126 | },
127 | },
128 | });
129 |
130 | paint(serializedRange) {
131 | this.marker.paint(serializedRange);
132 | Marker.clearSelection();
133 | this.highlights[serializedRange.uid] = serializedRange;
134 | this.mapHighlightIdToRange[serializedRange.uid] = this.marker.deserializeRange(
135 | serializedRange
136 | );
137 | this.saveHighlightsToLocalStorage();
138 | }
139 |
140 | setUserSelectionByRange(range) {
141 | let pos = range.getBoundingClientRect();
142 |
143 | let calculatedPos = {
144 | top: pos.top + window.scrollY,
145 | left: pos.left + window.scrollX,
146 | width: pos.width,
147 | height: pos.height,
148 | };
149 | if (calculatedPos.width === 0) {
150 | calculatedPos.width = 400;
151 | calculatedPos.top = window.pointerPos.y - 10;
152 | calculatedPos.left = window.pointerPos.x - 200;
153 | }
154 | this.setState({
155 | userSelection: {
156 | range,
157 | clientRect: calculatedPos,
158 | },
159 | hideHighlightButtons: false,
160 | });
161 | }
162 |
163 | mouseMoveListener = (e) => {
164 | const { pageX, pageY } = e;
165 | window.pointerPos = { x: pageX, y: pageY };
166 | };
167 | mouseupListener = (e) => {
168 | setTimeout(() => {
169 | this.handleMouseUp(e, false);
170 | });
171 | };
172 |
173 | handleMouseUp = (e, calledRecursively = false) => {
174 | try {
175 | const selection = window.getSelection();
176 | if (!selection.toString() && !demoMultiRangeMode) {
177 | this.setState({ hideHighlightButtons: true });
178 | return;
179 | }
180 |
181 | if (!selection.rangeCount) {
182 | return null;
183 | }
184 |
185 | let range = null;
186 | try {
187 | range = selection.getRangeAt(0);
188 | } catch (e) {}
189 | if (!range) {
190 | return null;
191 | }
192 |
193 | this.selectedHighlightId = null;
194 |
195 | this.setUserSelectionByRange(range);
196 | if (demoMultiRangeMode) {
197 | const serialized = this.marker.serializeRange(
198 | this.state.userSelection.range
199 | );
200 | if (!serialized) {
201 | return;
202 | }
203 | this.paint(serialized);
204 | }
205 | } finally {
206 | if (demoMultiRangeMode) {
207 | const selection = window.getSelection();
208 | if (selection.isCollapsed && !this.isHighlightElement(e.target)) {
209 | selection.modify("move", "forward", "character");
210 | selection.modify("extend", "backward", "word");
211 | selection.modify("move", "backward", "character");
212 | selection.modify("extend", "forward", "word");
213 | if (!calledRecursively) {
214 | this.handleMouseUp(e, true);
215 | }
216 | }
217 | setTimeout(() => {
218 | this.setState({
219 | hideHighlightButtons: Object.keys(this.highlights).length === 0,
220 | });
221 | });
222 | }
223 | }
224 | };
225 |
226 | componentDidMount() {
227 | if (!demoMultiRangeMode && localStorage[LOCALSTORAGE_HIGHLIGHTS_KEY]) {
228 | this.highlights = this.loadHighlightsFromLocalStorage();
229 | Object.keys(this.highlights).forEach((id) => {
230 | this.paint(this.highlights[id]);
231 | });
232 | }
233 |
234 | document.addEventListener("mouseup", this.mouseupListener);
235 | window.addEventListener("pointermove", this.mouseMoveListener);
236 | this.marker.addEventListeners();
237 | }
238 |
239 | componentWillUnmount() {
240 | document.removeEventListener("mouseup", this.mouseupListener);
241 | window.removeEventListener("pointermove", this.mouseMoveListener);
242 | this.marker.removeEventListeners();
243 | }
244 |
245 | render() {
246 | let markdown = parseMarkdown(this.props.markdown);
247 |
248 | const [top, left, width] = findProperTopLeftAndWidth(
249 | (this.state.userSelection || {}).clientRect
250 | );
251 | return (
252 |
253 | {!!this.state.userSelection &&
254 | !this.state.hideHighlightButtons &&
255 | ReactDOM.createPortal(
256 |
264 |
265 | {this.renderHighlightButtons()}
266 |
267 |
,
268 | document.body
269 | )}
270 |
271 |
272 |
273 | Here is a random number: {Math.random()}, try highlighting around
274 | it.
275 |
276 |
277 |
abc
278 |
279 | blackli sted
280 |
281 |
vcxdsf
282 |
qwerthgf
283 |
284 |
285 | 000000000000000000000000000000000000000000000000
286 |
287 | visibility hidden !! {" "}
288 | test
289 |
290 | 111111111111111111111111111111111111111111111111
291 |
292 |
296 | {markdown.content}
297 |
298 |
299 |
300 | );
301 | }
302 |
303 | renderHighlightButtons() {
304 | if (demoMultiRangeMode) {
305 | return this.renderHighlightButtonsMultiRangeMode();
306 | } else {
307 | return this.renderHighlightButtonsDefault();
308 | }
309 | }
310 |
311 | renderHighlightButtonsDefault() {
312 | const doHighlight = () => {
313 | const serialized = this.marker.serializeRange(
314 | this.state.userSelection.range
315 | );
316 | if (!serialized) {
317 | return;
318 | }
319 |
320 | this.paint(serialized);
321 |
322 | this.setState({ hideHighlightButtons: true });
323 | return { uid: serialized.uid };
324 | };
325 |
326 | const doDelete = () => {
327 | if (this.highlights[this.selectedHighlightId].annotation) {
328 | // eslint-disable-next-line no-restricted-globals
329 | if (!confirm("Are you sure? The annotation will be removed as well")) {
330 | return;
331 | }
332 | }
333 | this.marker.unpaint(this.highlights[this.selectedHighlightId]);
334 | delete this.highlights[this.selectedHighlightId];
335 | this.saveHighlightsToLocalStorage();
336 | this.setState({ hideHighlightButtons: true });
337 | };
338 |
339 | const doAnnotate = () => {
340 | if (!this.selectedHighlightId) {
341 | const result = doHighlight();
342 | this.selectedHighlightId = result.uid;
343 | }
344 | let selectedHighlightId = this.selectedHighlightId;
345 | showDialog(AnnotationDialog, {
346 | text: this.highlights[selectedHighlightId].annotation || "",
347 | }).then((data) => {
348 | this.highlights[selectedHighlightId].annotation = data;
349 | this.saveHighlightsToLocalStorage();
350 | this.marker.paintHighlights(selectedHighlightId);
351 | });
352 | };
353 |
354 | return (
355 | <>
356 | {!this.selectedHighlightId && (
357 |
358 | Highlight
359 |
360 | )}
361 | {!!this.selectedHighlightId && (
362 |
363 | Delete
364 |
365 | )}
366 |
367 | Annotate
368 |
369 | >
370 | );
371 | }
372 |
373 | renderHighlightButtonsMultiRangeMode() {
374 | const onSubmit = () => {
375 | const data = { id: makeid(), ranges: [] };
376 | Object.keys(this.highlights).forEach((key) => {
377 | data.ranges.push(this.highlights[key]);
378 | });
379 | alert("selection created: " + JSON.stringify(data, null, 4));
380 | onDiscard();
381 | };
382 | const onDiscard = () => {
383 | Object.keys(this.highlights).forEach((key) => {
384 | this.marker.unpaint(this.highlights[key]);
385 | delete this.highlights[key];
386 | });
387 | this.setState({
388 | hideHighlightButtons: true,
389 | });
390 | };
391 | return (
392 | <>
393 |
394 | Submit
395 |
396 |
397 | Discard
398 |
399 | >
400 | );
401 | }
402 |
403 | isHighlightElement(element) {
404 | try {
405 | return element.tagName === "HIGHLIGHT";
406 | } catch {
407 | return false;
408 | }
409 | }
410 | }
411 |
412 | class Content extends React.Component {
413 | shouldComponentUpdate(nextProps, nextState, nextContext) {
414 | return nextProps.markdown !== this.props.markdown;
415 | }
416 |
417 | render() {
418 | return (
419 |
420 | {this.props.children}
421 |
422 | );
423 | }
424 | }
425 |
426 | export default MarkdownRender;
427 |
--------------------------------------------------------------------------------
/src/lib/classes/Marker.ts:
--------------------------------------------------------------------------------
1 | import makeid from "../../utils/makeid";
2 | import EventHandler from "./EventHandler";
3 | import HighlightPainter from "./HighlightPainter";
4 | import EventHandlerContext from "./Context";
5 | import Context from "./Context";
6 | import SerializedRange from "./SerializedRange";
7 | import DeserializationError from "./errors/DeserializationError";
8 |
9 | const HighlightTagName = "web-marker-highlight";
10 | const HighlightBlacklistedElementClassName = "web-marker-black-listed-element";
11 | const AttributeNameHighlightId = "highlight-id";
12 |
13 | const defaultCharsToKeepForTextBeforeAndTextAfter = 128;
14 | const blackListedElementStyle = document.createElement("style");
15 | blackListedElementStyle.innerText = `.${HighlightBlacklistedElementClassName}, .MJX_Assistive_MathML>math>*, math>semantics>* {display:none!important;};`;
16 | interface MarkerConstructorArgs {
17 | rootElement?: HTMLElement;
18 | eventHandler?: EventHandler;
19 | highlightPainter?: HighlightPainter;
20 | }
21 |
22 | const defaultHighlightPainter: HighlightPainter = {
23 | paintHighlight: (context: Context, element: HTMLElement) => {
24 | console.log("paintHighlight", context, element);
25 | element.style.textDecoration = "underline";
26 | element.style.textDecorationColor = "orange";
27 | },
28 | };
29 |
30 | const defaultEventHandler: EventHandler = {
31 | onHighlightClick: (context, element) => {
32 | console.log("onHighlightClick", context, element);
33 | },
34 | onHighlightHoverStateChange: (context, element, hovering) => {
35 | console.log("onHighlightHoverStateChange", context, element);
36 | if (hovering) {
37 | element.style.backgroundColor = "#FFE49C";
38 | } else {
39 | element.style.backgroundColor = "";
40 | }
41 | },
42 | };
43 |
44 | class Marker {
45 | public static normalizeTextCache = {} as any;
46 | rootElement: Element;
47 | document: Document;
48 | window: Window;
49 | eventHandler: EventHandler;
50 | highlightPainter: HighlightPainter;
51 | state = {
52 | lastHoverId: "",
53 | uidToSerializedRange: {} as { [key: string]: SerializedRange },
54 | };
55 |
56 | constructor({
57 | rootElement,
58 | highlightPainter,
59 | eventHandler,
60 | }: MarkerConstructorArgs) {
61 | this.rootElement = rootElement || document.body;
62 | this.document = this.rootElement.getRootNode() as Document;
63 | this.window = this.document.defaultView as Window;
64 | this.highlightPainter = highlightPainter || defaultHighlightPainter;
65 | this.eventHandler = eventHandler || defaultEventHandler;
66 | }
67 |
68 | public static clearSelection(win: Window = window) {
69 | const selection = win.getSelection();
70 | if (!selection) {
71 | return;
72 | }
73 | if (selection.empty) {
74 | selection.empty();
75 | } else if (selection.removeAllRanges) {
76 | selection.removeAllRanges();
77 | }
78 | }
79 |
80 | private resolveHighlightElements(highlightId: string): HTMLElement[] {
81 | let elements: HTMLElement[] = [];
82 | for (let item of Array.from(
83 | this.document.getElementsByTagName(HighlightTagName)
84 | )) {
85 | if (item.getAttribute(AttributeNameHighlightId) === highlightId) {
86 | elements.push(item as HTMLElement);
87 | }
88 | }
89 | return elements;
90 | }
91 |
92 | private static normalizeText(s: string) {
93 | if (!Marker.normalizeTextCache[s]) {
94 | Marker.normalizeTextCache[s] = s.replace(/\s/g, "").toLowerCase();
95 | }
96 | return Marker.normalizeTextCache[s];
97 | }
98 |
99 | private static isBlackListedElementNode(element: Node | null) {
100 | if (!element) {
101 | return false;
102 | }
103 | if (element.nodeType !== Node.ELEMENT_NODE) {
104 | return false;
105 | }
106 | const computedStyle = getComputedStyle(element as any);
107 | if (computedStyle.display === "none") {
108 | return true;
109 | }
110 | if (computedStyle.visibility === "hidden") {
111 | return true;
112 | }
113 |
114 | const className = (element as any).className;
115 | if (
116 | className &&
117 | className.indexOf &&
118 | className.indexOf(HighlightBlacklistedElementClassName) >= 0
119 | ) {
120 | return true;
121 | }
122 |
123 | const tagName = (element as any).tagName;
124 | return (
125 | tagName === "STYLE" ||
126 | tagName === "SCRIPT" ||
127 | tagName === "TITLE" ||
128 | tagName === "NOSCRIPT" ||
129 | tagName === "SVG" ||
130 | tagName === "svg"
131 | );
132 | }
133 |
134 | private static getRealOffset(textNode: Node, normalizedOffset: number) {
135 | const s = textNode.textContent || "";
136 | let cumulative = 0;
137 | for (let i = 0; i < s.length; i++) {
138 | while (i < s.length && !Marker.normalizeText(s.substr(i, 1))) {
139 | // omit whitespaces
140 | i++;
141 | }
142 | if (cumulative === normalizedOffset) {
143 | return i;
144 | }
145 | cumulative++;
146 | }
147 | if (cumulative === normalizedOffset) {
148 | return s.length;
149 | }
150 | throw new Error("failed to get real offset");
151 | }
152 |
153 | private static unpaintElement(element: HTMLElement) {
154 | let childNodes = Array.from(element.childNodes);
155 | for (let i = 0; i < childNodes.length; i++) {
156 | element.parentNode?.insertBefore(childNodes[i], element);
157 | }
158 | element.parentNode?.removeChild(element);
159 | }
160 |
161 | private convertRangeToSelection(range: Range) {
162 | const selection = this.window.getSelection() as any;
163 | selection.removeAllRanges();
164 | selection.addRange(range);
165 | return selection;
166 | }
167 |
168 | public serializeRange(
169 | range: Range,
170 | options: { uid?: string; charsToKeepForTextBeforeAndTextAfter?: number } = {
171 | uid: undefined,
172 | charsToKeepForTextBeforeAndTextAfter:
173 | defaultCharsToKeepForTextBeforeAndTextAfter,
174 | }
175 | ): SerializedRange | null {
176 | this.document.head.appendChild(blackListedElementStyle);
177 |
178 | try {
179 | this.adjustRangeAroundBlackListedElement(range);
180 | const uid = options?.uid || makeid();
181 | const charsToKeepForTextBeforeAndTextAfter =
182 | options?.charsToKeepForTextBeforeAndTextAfter ||
183 | defaultCharsToKeepForTextBeforeAndTextAfter;
184 | const selection = this.convertRangeToSelection(range);
185 |
186 | let text = selection.toString();
187 | let textNormalized = Marker.normalizeText(text);
188 | if (textNormalized) {
189 | let textBefore = "";
190 | let textAfter = "";
191 |
192 | {
193 | // find textBefore
194 | textBefore =
195 | textBefore +
196 | this.getInnerText(range.startContainer).substr(
197 | 0,
198 | range.startOffset
199 | );
200 |
201 | let ptr = range.startContainer as Node | null;
202 | while (textBefore.length < charsToKeepForTextBeforeAndTextAfter) {
203 | ptr = this.findPreviousTextNodeInDomTree(ptr);
204 | if (!ptr) {
205 | // already reached the front
206 | break;
207 | }
208 | textBefore = (ptr as any).textContent + textBefore;
209 | }
210 | if (textBefore.length > charsToKeepForTextBeforeAndTextAfter) {
211 | textBefore = textBefore.substr(
212 | textBefore.length - charsToKeepForTextBeforeAndTextAfter
213 | );
214 | }
215 | }
216 |
217 | {
218 | // find textAfter
219 | textAfter =
220 | textAfter +
221 | this.getInnerText(range.endContainer).substr(range.endOffset);
222 |
223 | let ptr = range.endContainer as Node | null;
224 | while (textAfter.length < charsToKeepForTextBeforeAndTextAfter) {
225 | ptr = this.findNextTextNodeInDomTree(ptr);
226 | if (!ptr) {
227 | // already reached the end
228 | break;
229 | }
230 | textAfter = textAfter + (ptr as any).textContent;
231 | }
232 |
233 | if (textAfter.length > charsToKeepForTextBeforeAndTextAfter) {
234 | textAfter = textAfter.substr(
235 | 0,
236 | charsToKeepForTextBeforeAndTextAfter
237 | );
238 | }
239 | }
240 |
241 | this.state.uidToSerializedRange[uid] = {
242 | uid,
243 | textBefore,
244 | text,
245 | textAfter,
246 | };
247 | return this.state.uidToSerializedRange[uid];
248 | }
249 |
250 | return null;
251 | } finally {
252 | this.document.head.removeChild(blackListedElementStyle);
253 | }
254 | }
255 |
256 | public batchPaint(serializedRanges: SerializedRange[]) {
257 | const errors = {} as any;
258 | const { results: deserializedRanges, errors: deserializedRangeErrors } =
259 | this.batchDeserializeRange(serializedRanges);
260 |
261 | for (let i = 0; i < serializedRanges.length; i++) {
262 | if (deserializedRangeErrors[i]) {
263 | errors[i] = deserializedRangeErrors[i];
264 | continue;
265 | }
266 |
267 | const uid = serializedRanges[i].uid;
268 | const range = deserializedRanges[i];
269 |
270 | if (!range.collapsed) {
271 | const setElementHighlightIdAttribute = (element: HTMLElement) => {
272 | element.setAttribute(AttributeNameHighlightId, uid);
273 | };
274 |
275 | try {
276 | (() => {
277 | if (range.startContainer === range.endContainer) {
278 | if (range.startOffset === range.endOffset) {
279 | return;
280 | }
281 | // special case
282 | const word = (range.startContainer).splitText(
283 | range.startOffset
284 | );
285 | word.splitText(range.endOffset);
286 | setElementHighlightIdAttribute(
287 | this.convertTextNodeToHighlightElement(word)
288 | );
289 |
290 | return;
291 | }
292 |
293 | const toPaint = [];
294 | let ptr = (range.startContainer).splitText(
295 | range.startOffset
296 | ) as Node | null;
297 | toPaint.push(ptr);
298 |
299 | while (true) {
300 | ptr = this.findNextTextNodeInDomTree(ptr);
301 | if (ptr === range.endContainer) {
302 | break;
303 | }
304 | toPaint.push(ptr);
305 | }
306 |
307 | (range.endContainer).splitText(range.endOffset);
308 | toPaint.push(range.endContainer);
309 |
310 | toPaint.forEach((item) => {
311 | if (item) {
312 | let decoratedElement =
313 | this.convertTextNodeToHighlightElement(item);
314 | setElementHighlightIdAttribute(decoratedElement);
315 |
316 | if (!decoratedElement.innerText) {
317 | decoratedElement.parentElement?.insertBefore(
318 | item,
319 | decoratedElement.nextSibling
320 | );
321 | decoratedElement.parentElement?.removeChild(decoratedElement);
322 | }
323 | }
324 | });
325 |
326 | return;
327 | })();
328 |
329 | this.paintHighlights(uid);
330 | } catch (ex) {
331 | errors[i] = ex;
332 | }
333 | }
334 | }
335 |
336 | return { errors };
337 | }
338 |
339 | public paint(serializedRange: SerializedRange) {
340 | if (!serializedRange) {
341 | return;
342 | }
343 | const { errors } = this.batchPaint([serializedRange]);
344 | if (errors[0]) {
345 | throw errors[0];
346 | }
347 | }
348 |
349 | public unpaint(serializedRange: SerializedRange) {
350 | const id = serializedRange.uid;
351 |
352 | for (let element of this.resolveHighlightElements(id)) {
353 | Marker.unpaintElement(element);
354 | }
355 | }
356 |
357 | private batchDeserializeRange(serializedRanges: SerializedRange[]) {
358 | this.document.head.appendChild(blackListedElementStyle);
359 | const results = {} as any;
360 | const errors = {} as any;
361 | const rootText = this.getNormalizedInnerText(this.rootElement);
362 |
363 | for (let i = 0; i < serializedRanges.length; i++) {
364 | try {
365 | this.state.uidToSerializedRange[serializedRanges[i].uid] =
366 | serializedRanges[i];
367 | const offset = this.resolveSerializedRangeOffsetInText(
368 | rootText,
369 | serializedRanges[i]
370 | );
371 | const start = this.findElementAtOffset(this.rootElement, offset);
372 | const end = this.findElementAtOffset(
373 | this.rootElement,
374 | offset + Marker.normalizeText(serializedRanges[i].text).length
375 | );
376 | const range = this.document.createRange();
377 | range.setStart(
378 | start.element,
379 | Marker.getRealOffset(start.element, start.offset)
380 | );
381 | range.setEnd(
382 | end.element,
383 | Marker.getRealOffset(end.element, end.offset)
384 | );
385 | this.trimRangeSpaces(range);
386 | results[i] = range;
387 | } catch (ex) {
388 | errors[i] = ex;
389 | }
390 | }
391 |
392 | this.document.head.removeChild(blackListedElementStyle);
393 | return { results, errors };
394 | }
395 |
396 | public deserializeRange(serializedRange: SerializedRange) {
397 | const { results, errors } = this.batchDeserializeRange([serializedRange]);
398 | if (errors[0]) {
399 | throw errors[0];
400 | }
401 | return results[0];
402 | }
403 |
404 | clickListener = (e: Event) => {
405 | // the iframe HTMLElement instance is not same as other window HTMLElement instance
406 | if (!e.target || !(e.target instanceof (this.window as any).HTMLElement)) {
407 | return;
408 | }
409 |
410 | const target = e.target as HTMLElement;
411 | const id = target.getAttribute(AttributeNameHighlightId);
412 | if (id && this.eventHandler.onHighlightClick) {
413 | this.eventHandler.onHighlightClick(this.buildContext(id), target, e);
414 | }
415 | };
416 |
417 | mouseoverListener = (e: Event) => {
418 | if (!e.target || !(e.target instanceof (this.window as any).HTMLElement)) {
419 | return;
420 | }
421 |
422 | const target = e.target as HTMLElement;
423 | let newHoverId = target?.getAttribute(AttributeNameHighlightId);
424 | if (this.state.lastHoverId === newHoverId) {
425 | return;
426 | }
427 | const oldHoverId = this.state.lastHoverId;
428 | this.state.lastHoverId = newHoverId as string;
429 |
430 | if (newHoverId) {
431 | this.highlightHovering(newHoverId, true, e);
432 | }
433 | if (oldHoverId) {
434 | this.highlightHovering(oldHoverId, false, e);
435 | }
436 | };
437 |
438 | public addEventListeners() {
439 | this.rootElement.addEventListener("click", this.clickListener, true);
440 | this.rootElement.addEventListener(
441 | "mouseover",
442 | this.mouseoverListener,
443 | true
444 | );
445 | }
446 |
447 | public removeEventListeners() {
448 | this.rootElement.removeEventListener("click", this.clickListener, true);
449 | this.rootElement.removeEventListener(
450 | "mouseover",
451 | this.mouseoverListener,
452 | true
453 | );
454 | }
455 |
456 | public paintHighlights(highlightId: string) {
457 | let context = this.buildContext(highlightId);
458 | if (this.highlightPainter.beforePaintHighlight) {
459 | this.highlightPainter.beforePaintHighlight(context);
460 | }
461 | for (let element of this.resolveHighlightElements(highlightId)) {
462 | this.highlightPainter.paintHighlight(context, element);
463 | }
464 | if (this.highlightPainter.afterPaintHighlight) {
465 | this.highlightPainter.afterPaintHighlight(context);
466 | }
467 | }
468 |
469 | private highlightHovering(highlightId: string, hovering: boolean, e: Event) {
470 | for (let element of this.resolveHighlightElements(highlightId)) {
471 | if (this.eventHandler.onHighlightHoverStateChange) {
472 | this.eventHandler.onHighlightHoverStateChange(
473 | this.buildContext(highlightId),
474 | element as any,
475 | hovering,
476 | e
477 | );
478 | }
479 | }
480 | }
481 |
482 | private getInnerText(element: Node) {
483 | if (Marker.isBlackListedElementNode(element)) {
484 | return "";
485 | }
486 | if (element.nodeType === Node.TEXT_NODE) {
487 | return element.textContent;
488 | } else {
489 | if (typeof (element as any).innerText === "undefined") {
490 | let result = "";
491 | for (let i = 0; i < element.childNodes.length; i++) {
492 | result += this.getInnerText(element.childNodes[i]);
493 | }
494 | return result;
495 | } else {
496 | return (element as any).innerText;
497 | }
498 | }
499 | }
500 |
501 | private getNormalizedInnerText(element: Node) {
502 | return Marker.normalizeText(this.getInnerText(element));
503 | }
504 |
505 | private findLastChildTextNode(node: Node | null): Node | null {
506 | if (!node) {
507 | return null;
508 | }
509 | if (node.nodeType === Node.TEXT_NODE) {
510 | return node;
511 | }
512 | if (node.childNodes) {
513 | for (let i = node.childNodes.length - 1; i >= 0; i--) {
514 | if (Marker.isBlackListedElementNode(node.childNodes[i])) {
515 | continue;
516 | }
517 | const candidate = this.findLastChildTextNode(node.childNodes[i]);
518 | if (candidate !== null) {
519 | return candidate;
520 | }
521 | }
522 | }
523 | return null;
524 | }
525 |
526 | private findFirstChildTextNode(node: Node): Node | null {
527 | if (node.nodeType === Node.TEXT_NODE) {
528 | return node;
529 | }
530 | if (node.childNodes) {
531 | for (let i = 0; i < node.childNodes.length; i++) {
532 | if (Marker.isBlackListedElementNode(node.childNodes[i])) {
533 | continue;
534 | }
535 | const candidate = this.findFirstChildTextNode(node.childNodes[i]);
536 | if (candidate !== null) {
537 | return candidate;
538 | }
539 | }
540 | }
541 | return null;
542 | }
543 |
544 | private findPreviousTextNodeInDomTree(ptr: Node | null) {
545 | while (ptr) {
546 | while (Marker.isBlackListedElementNode(ptr?.previousSibling || null)) {
547 | ptr = ptr?.previousSibling || null;
548 | }
549 | while (ptr?.previousSibling) {
550 | const candidate = this.findLastChildTextNode(
551 | ptr?.previousSibling || null
552 | );
553 | if (candidate) {
554 | return candidate;
555 | }
556 | ptr = ptr.previousSibling;
557 | }
558 |
559 | ptr = ptr?.parentElement || null;
560 | }
561 | return null;
562 | }
563 |
564 | private findNextTextNodeInDomTree(ptr: Node | null) {
565 | while (ptr) {
566 | while (Marker.isBlackListedElementNode(ptr?.nextSibling || null)) {
567 | ptr = ptr?.nextSibling || null;
568 | }
569 | while (ptr?.nextSibling) {
570 | if (Marker.isBlackListedElementNode(ptr?.nextSibling)) {
571 | ptr = ptr.nextSibling;
572 | continue;
573 | }
574 | const candidate = this.findFirstChildTextNode(ptr.nextSibling);
575 | if (candidate) {
576 | return candidate;
577 | }
578 | ptr = ptr.nextSibling;
579 | }
580 |
581 | ptr = ptr?.parentElement || null;
582 | }
583 | return null;
584 | }
585 |
586 | private forwardOffset(
587 | { element, offset }: { element: Node; offset: number },
588 | toMove: number
589 | ): { element: Node; offset: number } {
590 | const elementText = this.getNormalizedInnerText(element);
591 | if (elementText.length > toMove + offset) {
592 | offset += toMove;
593 | return { element, offset };
594 | } else {
595 | let nextTextNode = this.findNextTextNodeInDomTree(element);
596 | if (nextTextNode) {
597 | return this.forwardOffset(
598 | {
599 | element: nextTextNode,
600 | offset: 0,
601 | },
602 | toMove - (elementText.length - offset)
603 | );
604 | } else {
605 | offset = this.getInnerText(element);
606 | return { element, offset };
607 | }
608 | }
609 | }
610 |
611 | private backwardOffset(
612 | { element, offset }: { element: Node; offset: number },
613 | toMove: number
614 | ): { element: Node; offset: number } {
615 | if (offset >= toMove) {
616 | offset -= toMove;
617 | return { element, offset };
618 | } else {
619 | const previousTextNode = this.findPreviousTextNodeInDomTree(element);
620 | if (previousTextNode) {
621 | return this.backwardOffset(
622 | {
623 | element: previousTextNode,
624 | offset: this.getNormalizedInnerText(previousTextNode).length,
625 | },
626 | toMove - offset
627 | );
628 | } else {
629 | offset = 0;
630 | return { element, offset };
631 | }
632 | }
633 | }
634 |
635 | private findElementAtOffset(
636 | root: Node,
637 | offset: number
638 | ): { element: Node; offset: number } {
639 | if (root.nodeType === Node.TEXT_NODE) {
640 | return { element: root as Text, offset: offset };
641 | } else {
642 | let cumulativeOffset = 0;
643 | for (let i = 0; i < root.childNodes.length; i++) {
644 | if (Marker.isBlackListedElementNode(root.childNodes[i])) {
645 | continue;
646 | }
647 | const childSize = this.getNormalizedInnerText(
648 | root.childNodes[i]
649 | ).length;
650 | cumulativeOffset += childSize;
651 | if (cumulativeOffset < offset) {
652 | continue;
653 | }
654 | return this.findElementAtOffset(
655 | root.childNodes[i],
656 | offset - (cumulativeOffset - childSize)
657 | );
658 | }
659 | throw new Error("failed to findElementAtOffset");
660 | }
661 | }
662 |
663 | private trimRangeSpaces(range: Range) {
664 | let start = this.getInnerText(range.startContainer).substr(
665 | range.startOffset
666 | );
667 | let startTrimmed = start.trimStart();
668 | range.setStart(
669 | range.startContainer,
670 | range.startOffset + (start.length - startTrimmed.length)
671 | );
672 |
673 | let end = this.getInnerText(range.endContainer).substr(0, range.endOffset);
674 | let endTrimmed = end.trimEnd();
675 | range.setEnd(
676 | range.endContainer,
677 | range.endOffset - (end.length - endTrimmed.length)
678 | );
679 | }
680 |
681 | private convertTextNodeToHighlightElement(word: Node) {
682 | const decoratedElement = this.document.createElement(HighlightTagName);
683 | word.parentElement?.insertBefore(decoratedElement, word.nextSibling);
684 | word.parentElement?.removeChild(word);
685 | decoratedElement.appendChild(word);
686 | return decoratedElement;
687 | }
688 |
689 | private buildContext(highlightId: string): EventHandlerContext {
690 | return {
691 | serializedRange: this.state.uidToSerializedRange[highlightId],
692 | marker: this,
693 | };
694 | }
695 |
696 | private adjustRangeAroundBlackListedElement(range: Range) {
697 | let ptr = range.startContainer;
698 | let blacklistedParentOfStartContainer = null;
699 | while (ptr) {
700 | if (Marker.isBlackListedElementNode(ptr)) {
701 | blacklistedParentOfStartContainer = ptr;
702 | }
703 | ptr = ptr.parentElement as any;
704 | }
705 |
706 | ptr = range.endContainer;
707 | let blacklistedParentOfEndContainer = null;
708 | while (ptr) {
709 | if (Marker.isBlackListedElementNode(ptr)) {
710 | blacklistedParentOfEndContainer = ptr;
711 | }
712 | ptr = ptr.parentElement as any;
713 | }
714 | if (
715 | blacklistedParentOfStartContainer &&
716 | blacklistedParentOfEndContainer &&
717 | blacklistedParentOfStartContainer === blacklistedParentOfEndContainer
718 | ) {
719 | throw new Error("cannot highlight blacklisted element");
720 | }
721 |
722 | if (blacklistedParentOfStartContainer) {
723 | range.setStart(
724 | this.findNextTextNodeInDomTree(
725 | blacklistedParentOfStartContainer
726 | ) as any,
727 | 0
728 | );
729 | }
730 | if (blacklistedParentOfEndContainer) {
731 | let prevNode = this.findPreviousTextNodeInDomTree(
732 | blacklistedParentOfEndContainer
733 | ) as any;
734 | range.setEnd(prevNode, this.getInnerText(prevNode).length);
735 | }
736 | }
737 |
738 | public getSerializedRangeFromUid(uid: string): SerializedRange | null {
739 | return this.state.uidToSerializedRange[uid] || null;
740 | }
741 |
742 | resolveSerializedRangeOffsetInText(
743 | text: any,
744 | serializedRange: SerializedRange
745 | ): number {
746 | // TODO: optimize algorithm, maybe use https://github.com/google/diff-match-patch
747 | const textBeforeNormalized = Marker.normalizeText(
748 | serializedRange.textBefore
749 | );
750 | const textAfterNormalized = Marker.normalizeText(serializedRange.textAfter);
751 | const textNormalized = Marker.normalizeText(serializedRange.text);
752 |
753 | for (let strategy of resolveSerializedRangeOffsetInTextStrategies) {
754 | const textToSearch =
755 | (strategy.textBefore ? textBeforeNormalized : "") +
756 | textNormalized +
757 | (strategy.textAfter ? textAfterNormalized : "");
758 |
759 | const index = text.indexOf(textToSearch);
760 | if (index >= 0) {
761 | return index + (strategy.textBefore ? textBeforeNormalized.length : 0);
762 | }
763 | }
764 |
765 | throw new DeserializationError(`failed to deserialize range`);
766 | }
767 | }
768 |
769 | const resolveSerializedRangeOffsetInTextStrategies = [
770 | {
771 | textBefore: true,
772 | textAfter: true,
773 | },
774 | {
775 | textBefore: false,
776 | textAfter: true,
777 | },
778 | {
779 | textBefore: true,
780 | textAfter: false,
781 | },
782 | {
783 | textBefore: false,
784 | textAfter: false,
785 | },
786 | ];
787 |
788 | export default Marker;
789 | export type { MarkerConstructorArgs };
790 |
--------------------------------------------------------------------------------