()
+
+ if (tr.state.field(selectiveLinesFacet)?.[0] != undefined) {
+ builder.add(tr.state.doc.line(1).from,
+ tr.state.doc.line(tr.state.field(selectiveLinesFacet)[0]).from, hiddenLine);
+ builder.add(tr.state.doc.line(tr.state.field(selectiveLinesFacet)[1]).to,
+ tr.state.doc.line(tr.newDoc.lines).to, hiddenLine);
+ }
+ const dec = builder.finish()
+ return dec;
+ },
+ provide: f => EditorView.decorations.from(f)
+})
+
+
+ export const selectiveLinesFacet = StateField.define<[number | undefined, number | undefined]>({
+ create: () => [undefined, undefined],
+ update(value, tr) {
+ if (tr.annotation(editableRange)) {
+ if (tr.annotation(editableRange)[0]) {
+ return [tr.annotation(editableRange)[0], Math.min(tr.state.doc.lines, tr.annotation(editableRange)[1])];
+ }
+ return tr.annotation(editableRange)
+ }
+ return value;
+ },
+
+})
+
+export const lineRangeToPosRange = (state: EditorState, range: [number, number]) => {
+ return {
+ from: state.doc.line(range[0]).from,
+ to: state.doc.line(range[1]+1).from,
+ }
+}
+
+export const smartDelete = EditorState.transactionFilter.of((tr:Transaction) => {
+ if(tr.isUserEvent('delete') && !tr.isUserEvent('delete.smart')){
+
+ const initialSelections = tr.startState.selection.ranges.map(range => ({
+ from: range.from,
+ to: range.to
+ }))
+
+ if(initialSelections.length > 0 && tr.startState.field(selectiveLinesFacet)?.[0])
+ {
+ const posRange = lineRangeToPosRange(tr.startState, tr.startState.field(selectiveLinesFacet));
+
+ tr.startState.update(
+ {
+ changes:{
+ from:Math.max(posRange.from, initialSelections[0].from),
+ to:Math.min(posRange.to, initialSelections[0].to),
+ },
+ annotations: Transaction.userEvent.of(`${tr.annotation(Transaction.userEvent)}.smart`)
+ });
+ }
+
+ }
+ return tr;
+
+ })
+
+ export const preventModifyTargetRanges = EditorState.transactionFilter.of((tr:Transaction) => {
+
+let newTrans = [];
+ try{
+ const selectiveLines = tr.startState.field(selectiveLinesFacet)
+
+
+ if (tr.isUserEvent('input') || tr.isUserEvent('delete') || tr.isUserEvent('move')) {
+ if (selectiveLines?.[0]) {
+ const posRange = lineRangeToPosRange(tr.startState, tr.startState.field(selectiveLinesFacet));
+ if (tr.changes.touchesRange(0, posRange.from-1) || !tr.changes.touchesRange(posRange.from, posRange.to)) {
+ return [];
+ }
+ }
+ }
+ if (tr.state.doc.lines != tr.startState.doc.lines) {
+
+
+ const numberNewLines = tr.state.doc.lines-tr.startState.doc.lines;
+ if (selectiveLines?.[0]) {
+ const posRange = lineRangeToPosRange(tr.startState, tr.startState.field(selectiveLinesFacet));
+ if (tr.changes.touchesRange(0, posRange.from-1)) {
+ newTrans.push(
+ {
+ annotations: [editableRange.of([selectiveLines[0]+numberNewLines, selectiveLines[1]+numberNewLines])]
+ })
+ } else
+ if (tr.changes.touchesRange(posRange.from-1, posRange.to)) {
+ newTrans.push(
+ {
+ annotations: [editableRange.of([selectiveLines[0], selectiveLines[1]+numberNewLines])]
+ });
+ }
+ }
+ }
+
+
+}
+catch(e){
+ return [];
+}
+return [tr, ...newTrans];
+ });
+
+ export const smartPaste = (getReadOnlyRanges:(targetState:EditorState)=>Array<{from:number|undefined, to:number|undefined}>) => EditorView.domEventHandlers({
+
+ paste(event, view)
+ {
+
+ const clipboardData = event.clipboardData || (window as any).clipboardData;
+ const pastedData = clipboardData.getData('Text');
+ const initialSelections = view.state.selection.ranges.map(range => ({
+ from: range.from,
+ to: range.to
+ }));
+
+ if(initialSelections.length > 0)
+ {
+ const readOnlyRanges = getReadOnlyRanges(view.state);
+ const result = getAvailableRanges(readOnlyRanges, initialSelections[0], {from: 0, to: view.state.doc.line(view.state.doc.lines).to}) as Array<{from:number, to:number}>;
+ if(result.length > 0)
+ {
+ view.dispatch(
+ {
+ changes:{
+ from: result[0].from,
+ to: result[0].to,
+ insert: pastedData
+ },
+ annotations: Transaction.userEvent.of(`input.paste.smart`)
+ })
+ }
+ }
+ }
+
+ })
+
+
+ const readOnlyRangesExtension = [smartDelete, preventModifyTargetRanges];
+ export const editBlockExtensions = () => [readOnlyRangesExtension, hideLine, selectiveLinesFacet]
+ export default readOnlyRangesExtension;
\ No newline at end of file
diff --git a/src/cm-extensions/inlineStylerView/InlineMenu.tsx b/src/cm-extensions/inlineStylerView/InlineMenu.tsx
new file mode 100644
index 0000000..47ae6e0
--- /dev/null
+++ b/src/cm-extensions/inlineStylerView/InlineMenu.tsx
@@ -0,0 +1,146 @@
+
+import MakeMDPlugin from "main"
+import { renderToStaticMarkup } from "react-dom/server"
+import * as ReactDOM from 'react-dom';
+import React, { useEffect, useMemo, useState } from 'react'
+import { EditorView } from '@codemirror/view'
+import 'css/InlineMenu.css'
+import t from "i18n"
+import { resolveStyles, InlineStyle } from "./styles";
+import { toggleMark } from "cm-extensions/inlineStylerView/marks";
+import { createRoot } from "react-dom/client";
+import { getActiveCM, getActiveMarkdownView } from "utils/codemirror";
+import { platformIsMobile } from "utils/utils";
+import { Mark } from "./Mark";
+import { markIconSet, uiIconSet } from "utils/icons";
+import MakeMenu from "components/MakeMenu/MakeMenu";
+import classNames from "classnames";
+
+export const loadStylerIntoContainer = (el: HTMLElement) => {
+ // el.removeChild(el.querySelector('.mobile-toolbar-options-container'))
+ const root = createRoot(el)
+ root.render()
+}
+
+export const InlineMenuComponent: React.FC <{cm?: EditorView, activeMarks: string[], mobile: boolean}> = (props) => {
+ const [mode, setMode] = useState(props.mobile ? 0 : 1);
+ const [colorMode, setColorMode] = useState<{prefix: string, suffix: string, closeTag: string} | null>(null);
+
+ const colors = ['#eb3b5a', '#fa8231', '#f7b731', '#20bf6b', '#0fb9b1', '#2d98da', '#3867d6', '#8854d0', '#4b6584']
+ const makeMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ const cm = props.cm ?? getActiveCM();
+ if (!cm)
+ return;
+ const end = cm.state.selection.main.to;
+ const insertChars = cm.state.sliceDoc(end-1, end) == cm.state.lineBreak ? window.make.settings.menuTriggerChar : cm.state.lineBreak+window.make.settings.menuTriggerChar;
+ cm.dispatch({
+ changes: {
+ from: end, to: end, insert: insertChars
+ }, selection: {
+ head: end+insertChars.length,
+ anchor: end+insertChars.length,
+ }
+ })
+ }
+ const toggleMarkAction = (e: React.MouseEvent, s: InlineStyle) => {
+ e.preventDefault();
+ const cm = props.cm ?? getActiveCM();
+ if (!cm)
+ return;
+ if (s.mark) {
+ cm.dispatch({
+ annotations: toggleMark.of(s.mark)
+ })
+ return;
+ }
+ const selection = cm.state.selection.main
+ const selectedText = cm.state.sliceDoc(selection.from, selection.to)
+ // cm.focus();
+ cm.dispatch({
+ changes: {from: selection.from, to: selection.to, insert: s.value.substring(0, s.insertOffset)+selectedText+s.value.substring(s.insertOffset)},
+ selection: s.cursorOffset ?
+ {anchor: selection.from+s.value.substring(0, s.insertOffset).length+selectedText.length+s.cursorOffset, head: selection.from+s.value.substring(0, s.insertOffset).length+selectedText.length+s.cursorOffset}
+ :
+ {anchor: selection.from+s.value.substring(0, s.insertOffset).length, head: selection.from+s.value.substring(0, s.insertOffset).length+selectedText.length}})
+ }
+
+ const makeMode = () => <>
+ {
+ makeMenu(e);
+ }} className='mk-mark' dangerouslySetInnerHTML={{__html: markIconSet['mk-make-slash']}}>
+
+ {
+ setMode(1);
+ }} className='mk-mark' dangerouslySetInnerHTML={{__html: markIconSet['mk-make-style']}}>
+
+ {
+ const view = getActiveMarkdownView();
+ window.make.app.commands.commands['editor:attach-file'].editorCallback(view.editor, view)
+ }} className='mk-mark' dangerouslySetInnerHTML={{__html: markIconSet['mk-make-attach']}}>
+
+ {
+ const view = getActiveMarkdownView();
+ window.make.app.commands.commands['editor:toggle-keyboard'].editorCallback(view.editor, view)
+ }} className='mk-mark' dangerouslySetInnerHTML={{__html: markIconSet['mk-make-keyboard']}}>
+
+ >
+
+const colorsMode = () => <>
+ {
+ setColorMode(null);
+ setMode(1);
+ }} dangerouslySetInnerHTML={{__html: uiIconSet['mk-ui-close']}}>
+
+{ colors.map((c, i) => {
+ setMode(1);
+ setColorMode(null)
+ const cm = props.cm ?? getActiveCM();
+ if (!cm)
+ return;
+ const selection = cm.state.selection.main
+ const selectedText = cm.state.sliceDoc(selection.from, selection.to)
+ cm.dispatch({
+ changes: {from: selection.from, to: selection.to, insert: colorMode.prefix+c+colorMode.suffix+selectedText+colorMode.closeTag},
+ })
+
+}} className='mk-color' style={{background: c}}>
) }>
+
+ const marksMode = () => <>
+ {
+ props.mobile ? {
+ setMode(0);
+ }} dangerouslySetInnerHTML={{__html: uiIconSet['mk-ui-close']}}>
+
: <>>}{
+ resolveStyles().map((s, i) =>
+ {
+ return f == s.mark) ? true : false} toggleMarkAction={toggleMarkAction}>
+ }) }
+ { window.make.settings.inlineStylerColors ?
+ <>
+
+ {
+ setMode(2);
+ setColorMode({prefix:``, closeTag:''})
+ }} className='mk-mark' dangerouslySetInnerHTML={{__html: markIconSet['mk-mark-color']}}>
+
+ {
+ setMode(2);
+ setColorMode({prefix:``, closeTag:''})
+ }} className='mk-mark' dangerouslySetInnerHTML={{__html: markIconSet['mk-mark-highlight']}}>
+
+ >
+ : <>>}
+ >
+
+
+ return e.preventDefault()}>
+ {
+ mode == 0 && props.mobile ?
+ makeMode() :
+ mode == 2 ?
+ colorsMode()
+ : marksMode()
+ }
+
+}
diff --git a/src/cm-extensions/inlineStylerView/Mark.tsx b/src/cm-extensions/inlineStylerView/Mark.tsx
new file mode 100644
index 0000000..af0df67
--- /dev/null
+++ b/src/cm-extensions/inlineStylerView/Mark.tsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import t from 'i18n'
+import { platformIsMobile } from 'utils/utils'
+import { InlineStyle } from './styles'
+import { markIconSet } from 'utils/icons'
+export const Mark = (props: {i: number, style: InlineStyle, active: boolean, toggleMarkAction: (e: React.MouseEvent, s: InlineStyle) => void}) => {
+ const {i, style, active, toggleMarkAction} = props;
+ //@ts-ignore
+ return toggleMarkAction(e, style)}
+ >
+
+}
\ No newline at end of file
diff --git a/src/cm-extensions/inlineStylerView/inlineStyler.tsx b/src/cm-extensions/inlineStylerView/inlineStyler.tsx
new file mode 100644
index 0000000..7354971
--- /dev/null
+++ b/src/cm-extensions/inlineStylerView/inlineStyler.tsx
@@ -0,0 +1,53 @@
+import {Tooltip, showTooltip, tooltips} from "cm-extensions/tooltip"
+import {StateField} from "@codemirror/state"
+import {EditorView} from "@codemirror/view"
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { createRoot } from 'react-dom/client'
+
+const cursorTooltipField = StateField.define({
+ create: getCursorTooltips,
+
+ update(tooltips, tr) {
+
+ if (!tr.docChanged && !tr.selection) return tooltips
+ return getCursorTooltips(tr.state)
+ },
+
+ provide: f => showTooltip.computeN([f], state => state.field(f))
+})
+import {EditorState} from "@codemirror/state"
+import { InlineMenuComponent } from "cm-extensions/inlineStylerView/InlineMenu"
+import { oMarks } from "cm-extensions/markSans/obsidianSyntax"
+import { rangeIsMark, expandRange } from "./marks"
+
+
+
+function getCursorTooltips(state: EditorState): readonly Tooltip[] {
+ return state.selection.ranges
+ .filter(range => !range.empty)
+ .map(range => {
+ const expandedRange = expandRange(range, state)
+ let line = state.doc.lineAt(range.head)
+ let activeMarks = oMarks.map(f => rangeIsMark(state, f, expandedRange) ? f.mark : '').filter(f => f != '')
+ return {
+ pos: Math.min(range.head, range.anchor),
+ above: true,
+ strictSide: true,
+ arrow: false,
+ create: (view: EditorView) => {
+ let dom = document.createElement("div")
+ dom.className = "cm-tooltip-cursor"
+ const reactElement = createRoot(dom)
+ reactElement.render(<>>)
+ return {dom}
+ }
+ }
+ })
+}
+
+
+export function cursorTooltip() {
+ return cursorTooltipField
+ }
+
\ No newline at end of file
diff --git a/src/cm-extensions/inlineStylerView/marks.ts b/src/cm-extensions/inlineStylerView/marks.ts
new file mode 100644
index 0000000..858c8ff
--- /dev/null
+++ b/src/cm-extensions/inlineStylerView/marks.ts
@@ -0,0 +1,163 @@
+import { syntaxTree } from '@codemirror/language';
+import { Annotation, EditorState, Transaction, SelectionRange, TransactionSpec, ChangeSpec } from '@codemirror/state'
+import { TransactionRange } from 'types/types';
+import { iterateTreeAtPos, iterateTreeInSelection } from 'utils/codemirror';
+import { oMark, oMarks } from '../markSans/obsidianSyntax';
+
+export const toggleMark = Annotation.define();
+
+const trimSpace = (pos: number, moveDirLeft: boolean, state: EditorState) => {
+ if (moveDirLeft && state.sliceDoc(pos, pos+1) == ' ')
+ return pos+1
+ if (!moveDirLeft && state.sliceDoc(pos-1, pos) == ' ')
+ return pos-1;
+ return pos
+}
+
+const newPosAfterFormatting = (pos: number, moveDirLeft: boolean, state: EditorState) => {
+ const line = state.doc.lineAt(pos);
+ const start = moveDirLeft ? line.from : pos
+ const end = moveDirLeft ? pos : line.to
+ let newPos = start;
+ let lastFormatPos = start;
+ let exitFormatRange = false;
+ iterateTreeInSelection({from: start, to: end}, state, {
+ enter: (node) => {
+
+ if (exitFormatRange)
+ return false;
+ if (node.name.contains('formatting')) {
+ if (!moveDirLeft && node.from > start) {
+ return false;
+ }
+ if (moveDirLeft) {
+ newPos = node.from
+ lastFormatPos = node.to;
+ } else {
+ newPos = node.to;
+ }
+ }
+
+ }
+ })
+ if (moveDirLeft && lastFormatPos < pos) {
+ newPos = pos;
+ }
+ return newPos
+}
+
+//move position to outside adjacent formatting marks, used for properly detect marked content ranges
+export const expandRange = (selection: TransactionRange, state: EditorState) : TransactionRange => {
+ const from = trimSpace(newPosAfterFormatting(selection.from, true, state), true, state)
+ const to = trimSpace(newPosAfterFormatting(selection.to, false, state), false, state)
+ return {from, to}
+}
+export const addMarkAtPos = (pos: number, mark: oMark) : TransactionSpec => ({changes: {from: pos, to: pos, insert: mark.formatChar}})
+
+export const rangeIsMark = (state: EditorState, mark: oMark, selection: TransactionRange) : boolean => posIsMark(selection.from, state, mark.mark) && posIsMark(selection.to, state, mark.mark);
+const posIsMark = (pos: number, state: EditorState, markString: string) : boolean => {
+ let isMark = false;
+ iterateTreeAtPos(pos, state, {
+ enter: ({name, from, to}) => {
+ if (nodeNameContainsMark(name, markString))
+ isMark = true;
+ }
+ })
+ return isMark
+}
+const nodeNameContainsMark = (name: string, markString: string) => {
+ return name.contains(markString)
+}
+export const edgeIsMark = (pos: number, state: EditorState, mark: oMark) => posIsMark(pos, state, mark.mark);
+ export const edgeIsMarkFormat = (pos: number, state: EditorState, mark: oMark) => posIsMark(pos, state, mark.formatting) ? true : (mark.altFormatting ? posIsMark(pos, state, mark.altFormatting) : false);
+
+export const transactionChangesForMark = (range: TransactionRange, mark: oMark, state: EditorState) => {
+ let newTrans = [];
+ if (rangeIsMark(state, mark, range)) {
+ if (edgeIsMarkFormat(range.from, state, mark) && !edgeIsMarkFormat(range.to, state, mark)) {
+ newTrans.push(addMarkAtPos(range.to, mark))
+ }
+ if (edgeIsMarkFormat(range.to, state, mark) && !edgeIsMarkFormat(range.from, state, mark)) {
+ newTrans.push(addMarkAtPos(range.from, mark))
+ }
+ } else
+ if (edgeIsMark(range.from, state, mark)) {
+ if (edgeIsMarkFormat(range.from, state, mark) && !edgeIsMark(range.from-1, state, mark)) {
+ newTrans.push(addMarkAtPos(range.from, mark))
+ }
+ newTrans.push(addMarkAtPos(range.to, mark))
+ } else
+ if (edgeIsMark(range.to, state, mark)) {
+ if (edgeIsMarkFormat(range.to, state, mark) && !edgeIsMark(range.to+1, state, mark)) {
+ newTrans.push(addMarkAtPos(range.to, mark))
+ }
+ newTrans.push(addMarkAtPos(range.from, mark))
+ } else {
+ newTrans.push(addMarkAtPos(range.to, mark))
+ newTrans.push(addMarkAtPos(range.from, mark))
+ }
+ return newTrans;
+ }
+
+ const removeAllInternalMarks = (sel: TransactionRange, state: EditorState, mark: oMark) : TransactionSpec => {
+ let returnTrans : ChangeSpec[] = [];
+ iterateTreeInSelection({from: sel.from, to: sel.to}, state, {
+ enter: ({name, from, to}) => {
+ if (nodeNameContainsMark(name, mark.formatting) || (mark.altFormatting ? nodeNameContainsMark(name, mark.altFormatting) : false))
+ returnTrans.push({
+ from, to: from+mark.formatChar.length
+ })
+ }
+ })
+ return {
+ changes: returnTrans
+ }
+ }
+export const toggleMarkExtension = EditorState.transactionFilter.of((tr: Transaction) => {
+ if (!tr.annotation(toggleMark))
+ return tr;
+
+ const markToggle = tr.annotation(toggleMark);
+ const mark = oMarks.find(f => f.mark == markToggle);
+ if (!mark) {
+ return tr;
+ }
+ const selection = tr.startState.selection.main;
+ let newTrans : TransactionSpec[] = [];
+ if (selection.head == selection.anchor) {
+
+ if (tr.startState.sliceDoc(selection.head-mark.formatChar.length, selection.head) == mark.formatChar && tr.startState.sliceDoc(selection.head, selection.head+mark.formatChar.length) == mark.formatChar) {
+ newTrans.push({
+ changes: {
+ from: selection.head-mark.formatChar.length,
+ to: selection.head+mark.formatChar.length
+ }
+ });
+ } else {
+ newTrans.push({
+ changes: {
+ from: selection.head,
+ insert: mark.formatChar + mark.formatChar
+ },
+ selection: {
+ anchor: selection.head+mark.formatChar.length,
+ head: selection.head+ mark.formatChar.length
+ }
+ })
+ }
+ return [tr, ...newTrans];
+
+ }
+
+
+
+ const range = expandRange(selection, tr.startState);
+
+
+ newTrans.push(removeAllInternalMarks(range, tr.startState, mark))
+ let newFrom = range.from;
+ let newTo = range.to;
+
+ newTrans.push(...transactionChangesForMark(range, mark, tr.startState))
+ return [tr, ...newTrans, {selection: {anchor: newFrom, head: newTo}}];
+});
\ No newline at end of file
diff --git a/src/cm-extensions/inlineStylerView/styles/default.ts b/src/cm-extensions/inlineStylerView/styles/default.ts
new file mode 100644
index 0000000..805792e
--- /dev/null
+++ b/src/cm-extensions/inlineStylerView/styles/default.ts
@@ -0,0 +1,45 @@
+export default [
+ {
+ label: "bold",
+ value: `****`,
+ insertOffset: 2,
+ icon: 'mk-mark-strong',
+ mark: 'strong'
+ },
+ {
+ label: "italics",
+ value: "**",
+ insertOffset: 1,
+ icon: 'mk-mark-em',
+ mark: 'em'
+
+ },
+ {
+ label: "strikethrough",
+ value: "~~~~",
+ insertOffset: 2,
+ icon: 'mk-mark-strikethrough',
+ mark: 'strikethrough'
+ },
+ {
+ label: "code",
+ value: "``",
+ insertOffset: 1,
+ icon: 'mk-mark-code',
+ mark: 'inline-code'
+ },
+ {
+ label: "link",
+ value: "[]()",
+ insertOffset: 1,
+ cursorOffset: 2,
+ icon: 'mk-mark-link'
+ },
+ {
+ label: "blocklink",
+ value: "[[]]",
+ insertOffset: 2,
+ icon: 'mk-mark-blocklink'
+ },
+
+]
diff --git a/src/cm-extensions/inlineStylerView/styles/index.ts b/src/cm-extensions/inlineStylerView/styles/index.ts
new file mode 100644
index 0000000..89ce466
--- /dev/null
+++ b/src/cm-extensions/inlineStylerView/styles/index.ts
@@ -0,0 +1,15 @@
+import MakeMDPlugin from "main"
+import defaultStyles from "./default"
+
+export type InlineStyle = {
+ label: string,
+ value: string,
+ insertOffset: number,
+ cursorOffset?: number,
+ icon: string,
+ mark?: string,
+}
+
+export function resolveStyles() {
+ return defaultStyles
+}
diff --git a/src/cm-extensions/markSans/callout.tsx b/src/cm-extensions/markSans/callout.tsx
new file mode 100644
index 0000000..20b6d9c
--- /dev/null
+++ b/src/cm-extensions/markSans/callout.tsx
@@ -0,0 +1,133 @@
+import {Decoration, WidgetType, EditorView} from '@codemirror/view'
+import {syntaxTree} from '@codemirror/language'
+import {ViewUpdate, ViewPlugin, DecorationSet} from "@codemirror/view"
+import {StateField, RangeSetBuilder, EditorState, Annotation } from '@codemirror/state'
+import { Editor } from 'obsidian';
+import { SyntaxNodeRef } from '@lezer/common';
+import { iterateTreeInDocument } from 'utils/codemirror';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { createFlowEditorInElement } from 'dispatch/flowDispatch';
+import { genId } from 'components/FlowEditor/FlowEditor';
+import { PortalType } from 'types/types';
+
+export const portalTypeAnnotation = Annotation.define();
+export const flowIDAnnotation = Annotation.define();
+export const flowIDStateField = StateField.define({
+ create: () => undefined,
+ update(value, tr) {
+ if (tr.annotation(flowIDAnnotation))
+ return tr.annotation(flowIDAnnotation)
+ return value;
+ },
+})
+
+export const flowTypeStateField = StateField.define({
+ create: () => 'none',
+ update(value, tr) {
+ if (tr.annotation(portalTypeAnnotation))
+ return tr.annotation(portalTypeAnnotation)
+ return value;
+ },
+})
+
+export const calloutField = StateField.define({
+ create() {
+ return Decoration.none
+ },
+ update(value, tr) {
+ if (tr.state.field(flowTypeStateField) != 'doc') {
+ return value;
+ }
+
+ let builder = new RangeSetBuilder()
+ let nodes = [] as {name: string, from: number, to: number}[];
+ syntaxTree(tr.state).iterate({
+
+ enter: ({name, from, to}) => {
+ nodes.push({name, from, to});
+ },
+ })
+ const nextQuote = (ns: {name: string, from: number, to: number}[], to: number) : number => {
+ const nq = ns.find(f => f.from == to+1 && f.name.contains('HyperMD-quote'))
+ if (nq) {
+ return nextQuote(ns, nq.to)
+ }
+ return to;
+ }
+ const previous = value.iter();
+ const previousSpecs = [] as {id: string, from: number, to: number}[];
+ while (previous.value !== null) {
+ previousSpecs.push(previous.value.spec.widget.info);
+ previous.next();
+ }
+ let index = 0;
+ nodes.map(({name, from, to}) => {
+ if (name.contains('HyperMD-callout')) {
+
+ const existingCallout = previousSpecs[index];
+ const endQuote = nextQuote(nodes, to)
+ const lineStart = tr.state.doc.lineAt(from).number
+ const lineEnd = tr.state.doc.lineAt(endQuote).number
+ if (existingCallout) {
+ builder.add(from, endQuote+1, calloutBlock({from: lineStart, to: lineEnd}, tr.state.sliceDoc(from, endQuote), existingCallout.id))
+ } else {
+ builder.add(from, endQuote+1, calloutBlock({from: lineStart, to: lineEnd}, tr.state.sliceDoc(from, endQuote), genId(8)))
+ }
+ index++;
+ }
+ })
+ const dec = builder.finish()
+ return dec;
+ },
+ provide: f => EditorView.decorations.from(f)
+ })
+
+export interface CalloutInfo {
+ range: {from: number, to: number}, readonly text: string, readonly id: string;
+}
+
+class CalloutWidget extends WidgetType {
+ constructor(readonly info: CalloutInfo) {
+ super();
+ }
+
+ eq(other: WidgetType) {
+ return (other as unknown as CalloutWidget).info.id === this.info.id;
+ }
+
+ toDOM() {
+ const parseTextToCallout = (text: string) : {icon: string, title: string} => {
+ if (!this.info.text) {
+ return {icon: '', title: ''};
+ }
+ const stringArray = text.split('\n');
+ const titleRegex = RegExp(/.*\[!(\w*)\]\s(.*)/)
+ const title = titleRegex.exec(stringArray[0]);
+ if (!title || title.length < 3) {
+ return {icon: '', title: ''};
+ }
+ return {
+ icon: title[1],
+ title: title[2],
+ }
+ }
+ const callOutData = parseTextToCallout(this.info.text);
+ const div = document.createElement('div');
+ div.toggleClass('callout', true);
+ const divTitle = div.createDiv('div');
+ divTitle.toggleClass('callout-title', true)
+ const div2 = div.createDiv('div');
+ div2.toggleClass('callout-content', true)
+ div2.setAttribute('id', "mk-callout-"+this.info.id)
+ // loadCalloutByDOM(div2, this.info.id)
+ return div;
+ }
+ }
+
+ const calloutBlock = (range: {from: number, to: number}, text: string, id: string) => Decoration.widget({
+ widget: new CalloutWidget({range, text, id}),
+ block: true,
+ })
+
+
diff --git a/src/cm-extensions/markSans/hr.ts b/src/cm-extensions/markSans/hr.ts
new file mode 100644
index 0000000..f32246b
--- /dev/null
+++ b/src/cm-extensions/markSans/hr.ts
@@ -0,0 +1,95 @@
+import {Decoration, WidgetType, DecorationSet, EditorView} from '@codemirror/view'
+import {StateField, RangeSetBuilder, EditorState} from '@codemirror/state'
+import { syntaxTree } from '@codemirror/language'
+import {iterateTreeInDocument, iterateTreeInSelection} from 'utils/codemirror'
+
+export const resetLine = Decoration.line({class: "mk-reset"})
+
+const needsReset = (state: EditorState, typeString: string, from: number, to: number) : boolean => {
+ const length = to-from;
+
+ if (typeString.contains('HyperMD-header')) {
+ //reset auto header without space
+ if (parseInt(typeString.replace(/.*HyperMD-header-(\d+).*/, '$1')) == length) {
+ return true;
+ }
+ let truefalse = true;
+ // reset Autoheader before hr
+ iterateTreeInSelection({from: from, to: to}, state, {
+ enter: ({type, from, to}) => {
+ if (type.name.contains('formatting-header')) {
+ truefalse = false;
+ }
+ }
+ })
+ return truefalse;
+ }
+ return false;
+}
+
+export const hrResetFix = StateField.define({
+ create() {
+ return Decoration.none
+ },
+ update(value, tr) {
+ let builder = new RangeSetBuilder()
+
+ iterateTreeInDocument(tr.state, {
+ enter: ({ type, from, to }) => {
+ if (needsReset(tr.state, type.name, from, to)) {
+ builder.add(from, from, resetLine)
+ }
+ }
+ });
+ const dec = builder.finish()
+ return dec;
+ },
+ provide: f => EditorView.decorations.from(f)
+ })
+
+const hrDecorations = (state: EditorState) : DecorationSet => {
+ let builder = new RangeSetBuilder()
+ let nodes = [] as {name: string, from: number, to: number}[];
+ iterateTreeInDocument(state, {
+ enter: ({name, from, to}) => {
+ if (name.contains('formatting-header') && state.sliceDoc(from, to) == '---' && !(state.selection.main.from >= from && state.selection.main.to <= to)) {
+ builder.add(from, to, hr)
+ }
+ }
+ })
+ const dec = builder.finish()
+ return dec;
+}
+
+export const hrField = StateField.define({
+ create(state) {
+ return hrDecorations(state)
+ },
+ update(value, tr) {
+ if (!tr.docChanged)
+ return value;
+ return hrDecorations(tr.state);
+ },
+ provide: f => EditorView.decorations.from(f)
+ })
+
+
+class HRWidget extends WidgetType {
+ constructor() {
+ super();
+ }
+
+ eq(other: WidgetType) {
+ return true;
+ }
+
+ toDOM() {
+ const div = document.createElement('hr');
+ return div;
+ }
+ }
+
+export const hr = Decoration.replace({
+ widget: new HRWidget(),
+ block: false,
+ })
\ No newline at end of file
diff --git a/src/cm-extensions/markSans/inlineSelection.ts b/src/cm-extensions/markSans/inlineSelection.ts
new file mode 100644
index 0000000..29be559
--- /dev/null
+++ b/src/cm-extensions/markSans/inlineSelection.ts
@@ -0,0 +1,161 @@
+import { Range, EditorState, Transaction, EditorSelection, TransactionSpec, StateField, RangeSetBuilder } from '@codemirror/state';
+import { expandRange, rangeIsMark, transactionChangesForMark } from 'cm-extensions/inlineStylerView/marks';
+import { TransactionRange } from 'types/types';
+import { checkRangeOverlap, iterateTreeAtPos, iterateTreeInDocument, iterateTreeInSelection, iterateTreeInVisibleRanges } from 'utils/codemirror';
+import { oMarks } from './obsidianSyntax';
+
+
+ export const inlineMakerDelete = EditorState.transactionFilter.of((tr: Transaction) => {
+ let newTrans = [] as TransactionSpec[];
+
+ if (!tr.isUserEvent('delete') || (!tr.isUserEvent('input') && tr.startState.selection.main.from != tr.startState.selection.main.to)){
+ return tr;
+ }
+
+ const changes = tr.changes;
+
+ changes.iterChanges((fromA, fromB, toA, toB, inserted) => {
+ const minFrom = Math.min(fromA, toA);
+ const maxTo = Math.max(fromA, toA);
+ const expandedRange = expandRange({from: minFrom, to: maxTo}, tr.startState);
+ let activeMarks = oMarks.filter(f => rangeIsMark(tr.startState, f, expandedRange))
+
+ const transactions = activeMarks.map(m => transactionChangesForMark(expandedRange, m, tr.startState))
+
+ newTrans.push({
+ changes: {
+ from: expandedRange.from,
+ to: expandedRange.to,
+ insert: inserted
+ }
+ })
+ newTrans.push(...transactions.reduce((p, c) => [...p, ...c], []));
+
+ return newTrans;
+ })
+
+ return [tr, ...newTrans];
+ });
+
+ const reverseSel = (t: TransactionRange) : TransactionRange => {
+ return {
+ from: t.to, to: t.from
+ }
+ }
+
+ const selFromTo = (from: number, to: number) : TransactionRange => {
+ return { from: from, to: to}
+ }
+
+
+
+ const pointSelection = (from: number, to: number, markLeft: boolean, pos: number, posDiff: number, userSelect: boolean) : TransactionRange | undefined =>
+
+ {
+
+ return checkLeftOfMark(from, pos) ? undefined :
+ checkMarkMiddle(from, to, pos) ? (markLeft && posDiff >= 1 || !markLeft && (posDiff != -1 || posDiff == -1 && !userSelect)) ?
+ selectBeforeMark(from, pos) : selectAfterMark(to+1) : undefined;
+ }
+
+ const checkLeftOfMark = (from: number, pos: number) : boolean => from == pos;
+ const checkMarkMiddle = (from: number, to: number, pos: number) : boolean => pos > from && pos <= to;
+ const selectBeforeMark = (from: number, pos: number) : TransactionRange => selFromTo(from, from)
+ const selectAfterMark = (to: number) : TransactionRange => selFromTo(to, to)
+
+ const inlinePositionMarkOffset = (typeString: string, from: number, to: number, state: EditorState) :{from: number, to: number, left: boolean, node: string} | undefined => {
+
+ const checkLeft = (from: number, to: number, formatString: string) : boolean => {
+ let left = true;
+ iterateTreeInSelection({from: from-2, to: to+2}, state, { enter: (node) => {
+
+ if (node.name.contains(formatString) && !(node.name.contains('formatting-'+formatString))) {
+ if (node.from < from) {
+ left = false;
+ }
+ if (node.to > to) {
+ left = true;
+ }
+
+ }
+ }})
+ return left;
+ }
+
+ if (typeString.contains('formatting-em') && !typeString.contains('formatting-embed')) {
+ return {from, to, left: checkLeft(from, to, 'em'), node: 'em'}
+ }
+ if (typeString.contains('formatting-strong')) {
+ return {from, to, left: checkLeft(from, to, 'strong'), node: 'strong'}
+ }
+ if (typeString.contains('formatting-strikethrough')) {
+ return {from, to, left: checkLeft(from, to, 'strikethrough'), node: 'strikethrough'}
+ }
+
+ if (typeString.contains('formatting-code')) {
+ if (!typeString.contains('hmd-codeblock')) {
+ return {from, to, left: checkLeft(from, to, 'inline-code'), node: 'inline-code'}
+ }
+ // return {from: from, to, left: checkLeft(from, to, 'HyperMD-codeblock')}
+
+ }
+
+ return undefined;
+ }
+
+
+
+export const inlineMakerSelect = EditorState.transactionFilter.of((tr:Transaction) => {
+
+ let newTrans : TransactionSpec[] = [];
+ if (!tr.isUserEvent('select')) {
+ return tr;
+ }
+ const selection = tr.newSelection.main
+ let lineNodes : {type: string, from: number, to: number}[] = [];
+ const minFrom = Math.min(selection.from, selection.to);
+ const maxTo = Math.max(selection.from, selection.to);
+ if (minFrom != maxTo) {
+ const newRange = expandRange({from: minFrom, to: maxTo}, tr.state);
+ const fixedRange = (minFrom == selection.anchor) ? newRange : reverseSel(newRange);
+ newTrans.push({selection: {anchor: fixedRange.from, head: fixedRange.to}})
+ } else {
+ iterateTreeInSelection({from: tr.state.doc.lineAt(minFrom).from, to: tr.state.doc.lineAt(maxTo).to}, tr.state, {
+ enter: ({ type, from, to }) => {
+ lineNodes.push({type: type.name, from, to});
+ }
+ })
+ const fixSel = (oldSel: TransactionRange | undefined, anchor: number) : TransactionRange | undefined => {
+ let mark : {from: number, to: number, left: boolean, node: string};
+ let newSel : TransactionRange;
+ const head = oldSel.from == anchor ? oldSel.to : oldSel.from;
+ for (let node of lineNodes) {
+ if (node.from <= head && node.to >= head) {
+ mark = inlinePositionMarkOffset(node.type, node.from, node.to, tr.state);
+ if (mark)
+ break;
+ }
+ }
+ if (mark) {
+ newSel = pointSelection(mark.from, mark.to, mark.left, oldSel.from, tr.startState.selection.main.from-selection.from, tr.isUserEvent('select'))
+ }
+ if (!newSel || newSel.from == oldSel.from && newSel.to == oldSel.to) {
+
+ if (oldSel.to == anchor)
+ return reverseSel(oldSel)
+ return oldSel;
+ }
+ return fixSel(newSel, anchor);
+ }
+
+ const selChange = fixSel({from: minFrom, to: maxTo}, selection.anchor);
+
+ if (selChange) {
+ newTrans.push({
+ selection: {anchor: selChange.from, head: selChange.to}
+ })
+ }
+ }
+ return [tr, ...newTrans];
+});
+
diff --git a/src/cm-extensions/markSans/obsidianSyntax.ts b/src/cm-extensions/markSans/obsidianSyntax.ts
new file mode 100644
index 0000000..72d41ac
--- /dev/null
+++ b/src/cm-extensions/markSans/obsidianSyntax.ts
@@ -0,0 +1,35 @@
+export type oMark = {
+ mark: string;
+ formatting: string;
+ formatChar: string;
+ altFormatting?: string;
+}
+export const oMarks : oMark[] = [
+ {
+ mark: 'em',
+ formatting: 'formatting-em',
+ altFormatting: 'em_formatting_formatting-strong',
+ formatChar: '*'
+ },
+ {
+ mark: 'strong',
+ formatting: 'formatting-strong',
+ formatChar: '**'
+ },
+ {
+ mark: 'strikethrough',
+ formatting: 'formatting-strikethrough',
+ formatChar: '~~'
+ },
+ {
+ mark: 'inline-code',
+ formatting: 'formatting-code',
+ formatChar: '`'
+ }
+]
+
+export type oBlock = {
+ block: string;
+ formatting: string;
+ blockChar: string;
+}
\ No newline at end of file
diff --git a/src/cm-extensions/markSans/selection.ts b/src/cm-extensions/markSans/selection.ts
new file mode 100644
index 0000000..2dd0bd2
--- /dev/null
+++ b/src/cm-extensions/markSans/selection.ts
@@ -0,0 +1,161 @@
+import { Range, EditorState, Transaction, EditorSelection, TransactionSpec, StateField, RangeSetBuilder } from '@codemirror/state';
+import {
+ Decoration,
+ DecorationSet,
+ EditorView,
+ ViewPlugin,
+ ViewUpdate
+} from '@codemirror/view';
+import { checkRangeOverlap, iterateTreeInDocument, iterateTreeInSelection, iterateTreeInVisibleRanges } from 'utils/codemirror';
+import { hrResetFix } from './hr';
+
+
+
+
+ export const makerDelete = EditorState.transactionFilter.of((tr: Transaction) => {
+ let newTrans = [] as TransactionSpec[];
+
+
+ if (tr.isUserEvent('delete.forward')){
+ }
+
+ if (tr.isUserEvent('delete.backward') && !tr.isUserEvent('delete.selection') && !tr.isUserEvent('delete.selection.smart')) {
+
+ const selection = tr.newSelection.main;
+ iterateTreeInSelection(selection, tr.startState, {
+ enter: ({ type, from, to }) => {
+
+ const mark = positionMarkOffset(type.name, from, to, tr.startState);
+
+ if (mark) {
+ if (!hasReset(tr.startState, from, to)) {
+ newTrans.push(pointDeletion(tr, mark.from, mark.to, selection.from))
+ }
+ }
+ }
+ });
+ }
+ return [tr, ...newTrans];
+ });
+
+ const reverseSel = (t: TransactionSpec) => {
+ const sel = t.selection as EditorSelection;
+ return {selection: EditorSelection.single(sel.main.head, sel.main.anchor)
+ }
+ }
+
+ const selFromTo = (from: number, to: number) => {
+ return { selection: EditorSelection.single(from, to)
+ }
+ }
+ const delFromTo = (tr: Transaction, from: number, to: number) => {
+ return { changes: {from, to},
+ annotations: Transaction.userEvent.of(`${tr.annotation(Transaction.userEvent)}.smart`)
+ }
+ }
+ const pointDeletion = (tr: Transaction, from: number, to: number, pos: number) : TransactionSpec =>
+ checkMarkMiddle(from, to, pos) ? deleteMark(tr, from, pos) : {};
+ const deleteMark = (tr: Transaction, from: number, pos: number) : TransactionSpec => from == 0 ? delFromTo(tr, from, pos) : delFromTo(tr, from, pos)
+ const changeSelectionToPrevLine = (from: number, head: number) : TransactionSpec => selFromTo(from, head);
+ const changeSelectionToEndPrevLine = (from: number, head: number) : TransactionSpec => selFromTo(from, head-1);
+ const changeSelectionToAfterMark = (head: number, to: number) : TransactionSpec => selFromTo(head, to);
+ const changeSelectionToMark = (to: number, head: number) : TransactionSpec => selFromTo(to, head);
+ const rangeBeginsInMark = (from: number, to: number, pos: number, ) : boolean => pos >= from && pos < to;
+ const rangeEndsAtMark = (from: number, to: number, pos: number, ) : boolean => pos == from;
+ const rangeBeginsBefore = (from: number, to: number, anchor: number, head: number, ) : boolean => head == from-1;
+ const pointSelection = (from: number, to: number, pos: number, left: boolean) : TransactionSpec => checkLineStart(from, pos) ? to-from == 1 &&
+ left ? selectPreviousLine(from, pos) : selectLineStart(to) :
+ checkMarkMiddle(from, to, pos) ? left && checkMarkMiddleRightMost(from, to, pos) ? selectPreviousLine(from, pos) : selectLineStart(to) : {};
+
+ const checkLineStart = (from: number, pos: number) : boolean => from == pos;
+ const checkMarkMiddle = (from: number, to: number, pos: number) : boolean => pos > from && pos < to;
+ const checkMarkMiddleRightMost = (from: number, to: number, pos: number) : boolean => pos == to-1;
+ const selectPreviousLine = (from: number, pos: number) : TransactionSpec => from == 0 ? selFromTo(pos, pos) : selFromTo(from-1, from-1)
+ const selectLineStart = (to: number) : TransactionSpec => selFromTo(to, to)
+
+ const positionMarkOffset = (typeString: string, from: number, to: number, state: EditorState) : {from: number, to: number} | undefined => {
+
+
+ if (typeString.contains('HyperMD-header')) {
+ return {from, to: from+parseInt(typeString.replace(/.*HyperMD-header-(\d+).*/, '$1'))+1}
+ }
+ if (typeString.contains('HyperMD-task-line')) {
+ return {from, to: from+parseInt(typeString.replace(/.*HyperMD-list-line-(\d+).*/, '$1'))+5}
+ }
+ if (typeString.contains('formatting-list-ol')) {
+ let returnMark = undefined;
+ iterateTreeInSelection({from: from, to: to}, state, {
+ enter: ({type, from, to}) => {
+ if (type.name.contains('HyperMD-list-line')) {
+ returnMark = {from, to: from+parseInt(type.name.replace(/.*HyperMD-list-line-(\d+).*/, '$1'))+2};
+ }
+ }
+ })
+ return returnMark;
+ }
+ if (typeString.contains('HyperMD-list-line')) {
+ return {from, to: from+parseInt(typeString.replace(/.*HyperMD-list-line-(\d+).*/, '$1'))+1}
+ }
+ if (typeString.contains('HyperMD-quote') && !typeString.contains('HyperMD-quote-lazy')) {
+ return {from, to: from+1}
+ }
+
+ return undefined;
+ }
+ const rangeSelection = (from: number, to: number, anchor: number, head: number, ) : TransactionSpec => {
+ const minFrom = Math.min(anchor, head);
+ const maxTo = Math.max(anchor, head);
+
+ /*if (rangeBeginsBefore(from, to, maxTo, minFrom)) {
+ const newSel = changeSelectionToMark(to, maxTo);
+ return minFrom == head ? newSel : reverseSel(newSel);
+ }*/
+ if (rangeEndsAtMark(from, to, maxTo)) {
+ const newSel = changeSelectionToAfterMark(minFrom, to);
+ return minFrom == anchor ? newSel : reverseSel(newSel);
+ }
+ if (rangeBeginsInMark(from, to, maxTo)) {
+ const newSel = changeSelectionToEndPrevLine(minFrom, from);
+ return minFrom == anchor ? newSel : reverseSel(newSel);
+ }
+ if (rangeBeginsInMark(from, to, minFrom)) {
+ const newSel = changeSelectionToMark(to, maxTo);
+ return minFrom == head ? newSel : reverseSel(newSel);
+ }
+ return {};
+ }
+
+
+ const hasReset = (state: EditorState, from: number, to: number) : boolean => {
+ let trueFalse = false;
+ state.field(hrResetFix, false)?.between(from, to, (f,t,v) => {
+ trueFalse = true;
+ });
+ return trueFalse;
+ }
+
+export const makerSelect = EditorState.transactionFilter.of((tr:Transaction) => {
+ let newTrans = [] as TransactionSpec[];
+ if (tr.isUserEvent('delete') || tr.isUserEvent('input')) {
+ return tr;
+ }
+ const selection = tr.newSelection.main
+ if (selection.from == 0 && selection.to == 0)
+ return tr;
+ iterateTreeInSelection(selection, tr.state, {
+ enter: ({ type, from, to }) => {
+
+ const mark = positionMarkOffset(type.name, from, to, tr.state);
+
+ if (mark) {
+ if (!hasReset(tr.state, from, to))
+ newTrans.push((selection.from != selection.to) ?
+ rangeSelection(mark.from, mark.to, selection.from, selection.to) :
+ pointSelection(mark.from, mark.to, selection.from, tr.startState.selection.main.from == selection.from+1))
+ }
+ }
+ });
+// return tr;
+ return [tr, ...newTrans];
+});
+
diff --git a/src/cm-extensions/placeholder.ts b/src/cm-extensions/placeholder.ts
new file mode 100644
index 0000000..3e0b239
--- /dev/null
+++ b/src/cm-extensions/placeholder.ts
@@ -0,0 +1,20 @@
+import { EditorView, Decoration, DecorationSet, } from "@codemirror/view";
+import t from 'i18n'
+import { StateField, RangeSetBuilder } from "@codemirror/state";
+const placeholderLine = Decoration.line({attributes: {'data-ph': t.labels.placeholder}, class: 'cm-placeholder'})
+
+export const placeholder = StateField.define({
+ create() {
+ return Decoration.none
+ },
+ update(value, tr) {
+ let builder = new RangeSetBuilder()
+ const currentLine = tr.state.doc.lineAt(tr.state.selection.main.head);
+
+ if (currentLine?.length == 0)
+ builder.add(currentLine.from, currentLine.from, placeholderLine);
+ const dec = builder.finish()
+ return dec;
+ },
+ provide: f => EditorView.decorations.from(f)
+ })
\ No newline at end of file
diff --git a/src/cm-extensions/tooltip.ts b/src/cm-extensions/tooltip.ts
new file mode 100644
index 0000000..0b3265e
--- /dev/null
+++ b/src/cm-extensions/tooltip.ts
@@ -0,0 +1,679 @@
+import {EditorView, ViewPlugin, ViewUpdate, Direction, logException} from "@codemirror/view"
+import {EditorState, StateEffect, StateEffectType, Facet, StateField, Extension, MapMode} from "@codemirror/state"
+
+//FORK OF CODEMIRROR TOOLTIP TO FIX HOVER
+
+const ios = typeof navigator != "undefined" &&
+ !/Edge\/(\d+)/.exec(navigator.userAgent) && /Apple Computer/.test(navigator.vendor) &&
+ (/Mobile\/\w+/.test(navigator.userAgent) || navigator.maxTouchPoints > 2)
+
+type Rect = {left: number, right: number, top: number, bottom: number}
+
+type Measured = {
+ editor: DOMRect,
+ parent: DOMRect,
+ pos: (Rect | null)[],
+ size: DOMRect[],
+ space: {left: number, top: number, right: number, bottom: number}
+}
+
+const Outside = "-10000px"
+
+const enum Arrow { Size = 7, Offset = 14 }
+
+class TooltipViewManager {
+ private input: readonly (Tooltip | null)[]
+ tooltips: readonly Tooltip[]
+ tooltipViews: readonly TooltipView[]
+
+ constructor(
+ view: EditorView,
+ private readonly facet: Facet,
+ private readonly createTooltipView: (tooltip: Tooltip) => TooltipView
+ ) {
+ this.input = view.state.facet(facet)
+ this.tooltips = this.input.filter(t => t) as Tooltip[]
+ this.tooltipViews = this.tooltips.map(createTooltipView)
+ }
+
+ update(update: ViewUpdate) {
+ let input = update.state.facet(this.facet)
+ let tooltips = input.filter(x => x) as Tooltip[]
+ if (input === this.input) {
+ for (let t of this.tooltipViews) if (t.update) t.update(update)
+ return false
+ }
+
+ let tooltipViews = []
+ for (let i = 0; i < tooltips.length; i++) {
+ let tip = tooltips[i], known = -1
+ if (!tip) continue
+ for (let i = 0; i < this.tooltips.length; i++) {
+ let other = this.tooltips[i]
+ if (other && other.create == tip.create) known = i
+ }
+ if (known < 0) {
+ tooltipViews[i] = this.createTooltipView(tip)
+ } else {
+ let tooltipView = tooltipViews[i] = this.tooltipViews[known]
+ if (tooltipView.update) tooltipView.update(update)
+ }
+ }
+ for (let t of this.tooltipViews) if (tooltipViews.indexOf(t) < 0) t.dom.remove()
+
+ this.input = input
+ this.tooltips = tooltips
+ this.tooltipViews = tooltipViews
+ return true
+ }
+}
+
+/// Return an extension that configures tooltip behavior.
+export function tooltips(config: {
+ /// By default, tooltips use `"fixed"`
+ /// [positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
+ /// which has the advantage that tooltips don't get cut off by
+ /// scrollable parent elements. However, CSS rules like `contain:
+ /// layout` can break fixed positioning in child nodes, which can be
+ /// worked about by using `"absolute"` here.
+ ///
+ /// On iOS, which at the time of writing still doesn't properly
+ /// support fixed positioning, the library always uses absolute
+ /// positioning.
+ position?: "fixed" | "absolute",
+ /// The element to put the tooltips into. By default, they are put
+ /// in the editor (`cm-editor`) element, and that is usually what
+ /// you want. But in some layouts that can lead to positioning
+ /// issues, and you need to use a different parent to work around
+ /// those.
+ parent?: HTMLElement
+ /// By default, when figuring out whether there is room for a
+ /// tooltip at a given position, the extension considers the entire
+ /// space between 0,0 and `innerWidth`,`innerHeight` to be available
+ /// for showing tooltips. You can provide a function here that
+ /// returns an alternative rectangle.
+ tooltipSpace?: (view: EditorView) => {top: number, left: number, bottom: number, right: number}
+} = {}): Extension {
+ return tooltipConfig.of(config)
+}
+
+type TooltipConfig = {
+ position: "fixed" | "absolute",
+ parent: ParentNode | null,
+ tooltipSpace: (view: EditorView) => {top: number, left: number, bottom: number, right: number}
+}
+
+function windowSpace() {
+ return {top: 0, left: 0, bottom: innerHeight, right: innerWidth}
+}
+
+const tooltipConfig = Facet.define, TooltipConfig>({
+ combine: values => ({
+ position: ios ? "absolute" : values.find(conf => conf.position)?.position || "fixed",
+ parent: values.find(conf => conf.parent)?.parent || null,
+ tooltipSpace: values.find(conf => conf.tooltipSpace)?.tooltipSpace || windowSpace,
+ })
+})
+
+const tooltipPlugin = ViewPlugin.fromClass(class {
+ manager: TooltipViewManager
+ measureReq: {read: () => Measured, write: (m: Measured) => void, key: any}
+ inView = true
+ position: "fixed" | "absolute"
+ parent: ParentNode | null
+ container!: HTMLElement
+ classes: string
+ intersectionObserver: IntersectionObserver | null
+ lastTransaction = 0
+ measureTimeout = -1
+
+ constructor(readonly view: EditorView) {
+ let config = view.state.facet(tooltipConfig)
+ this.position = config.position
+ this.parent = config.parent
+ this.classes = view.themeClasses
+ this.createContainer()
+ this.measureReq = {read: this.readMeasure.bind(this), write: this.writeMeasure.bind(this), key: this}
+ this.manager = new TooltipViewManager(view, showTooltip, t => this.createTooltip(t))
+ this.intersectionObserver = typeof IntersectionObserver == "function" ? new IntersectionObserver(entries => {
+ if (Date.now() > this.lastTransaction - 50 &&
+ entries.length > 0 && entries[entries.length - 1].intersectionRatio < 1)
+ this.measureSoon()
+ }, {threshold: [1]}) : null
+ this.observeIntersection()
+ view.dom.ownerDocument.defaultView?.addEventListener("resize", this.measureSoon = this.measureSoon.bind(this))
+ this.maybeMeasure()
+ }
+
+ createContainer() {
+ if (this.parent) {
+ this.container = document.createElement("div")
+ this.container.style.position = "relative"
+ this.container.className = this.view.themeClasses
+ this.parent.appendChild(this.container)
+ } else {
+ this.container = this.view.dom
+ }
+ }
+
+ observeIntersection() {
+ if (this.intersectionObserver) {
+ this.intersectionObserver.disconnect()
+ for (let tooltip of this.manager.tooltipViews)
+ this.intersectionObserver.observe(tooltip.dom)
+ }
+ }
+
+ measureSoon() {
+ //@ts-ignore
+ if (this.measureTimeout < 0) this.measureTimeout = setTimeout(() => {
+ this.measureTimeout = -1
+ this.maybeMeasure()
+ }, 50)
+ }
+
+ update(update: ViewUpdate) {
+ if (update.transactions.length) this.lastTransaction = Date.now()
+ let updated = this.manager.update(update)
+ if (updated) this.observeIntersection()
+ let shouldMeasure = updated || update.geometryChanged
+ let newConfig = update.state.facet(tooltipConfig)
+ if (newConfig.position != this.position) {
+ this.position = newConfig.position
+ for (let t of this.manager.tooltipViews) t.dom.style.position = this.position
+ shouldMeasure = true
+ }
+ if (newConfig.parent != this.parent) {
+ if (this.parent) this.container.remove()
+ this.parent = newConfig.parent
+ this.createContainer()
+ for (let t of this.manager.tooltipViews) this.container.appendChild(t.dom)
+ shouldMeasure = true
+ } else if (this.parent && this.view.themeClasses != this.classes) {
+ this.classes = this.container.className = this.view.themeClasses
+ }
+ if (shouldMeasure) this.maybeMeasure()
+ }
+
+ createTooltip(tooltip: Tooltip) {
+ let tooltipView = tooltip.create(this.view)
+ tooltipView.dom.classList.add("cm-tooltip")
+ if (tooltip.arrow && !tooltipView.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")) {
+ let arrow = document.createElement("div")
+ arrow.className = "cm-tooltip-arrow"
+ tooltipView.dom.appendChild(arrow)
+ }
+ tooltipView.dom.style.position = this.position
+ tooltipView.dom.style.top = Outside
+ this.container.appendChild(tooltipView.dom)
+ if (tooltipView.mount) tooltipView.mount(this.view)
+ return tooltipView
+ }
+
+ destroy() {
+ this.view.dom.ownerDocument.defaultView?.removeEventListener("resize", this.measureSoon)
+ for (let {dom} of this.manager.tooltipViews) dom.remove()
+ this.intersectionObserver?.disconnect()
+ clearTimeout(this.measureTimeout)
+ }
+
+ readMeasure() {
+ let editor = this.view.dom.getBoundingClientRect()
+ return {
+ editor,
+ parent: this.parent ? this.container.getBoundingClientRect() : editor,
+ pos: this.manager.tooltips.map((t, i) => {
+ let tv = this.manager.tooltipViews[i]
+ return tv.getCoords ? tv.getCoords(t.pos) : this.view.coordsAtPos(t.pos)
+ }),
+ size: this.manager.tooltipViews.map(({dom}) => dom.getBoundingClientRect()),
+ space: this.view.state.facet(tooltipConfig).tooltipSpace(this.view),
+ }
+ }
+
+ writeMeasure(measured: Measured) {
+ let {editor, space} = measured
+ let others = []
+ for (let i = 0; i < this.manager.tooltips.length; i++) {
+ let tooltip = this.manager.tooltips[i], tView = this.manager.tooltipViews[i], {dom} = tView
+ let pos = measured.pos[i], size = measured.size[i]
+ // Hide tooltips that are outside of the editor.
+ if (!pos || pos.bottom <= Math.max(editor.top, space.top) ||
+ pos.top >= Math.min(editor.bottom, space.bottom) ||
+ pos.right < Math.max(editor.left, space.left) - .1 ||
+ pos.left > Math.min(editor.right, space.right) + .1) {
+ dom.style.top = Outside
+ continue
+ }
+ let arrow: HTMLElement | null = tooltip.arrow ? tView.dom.querySelector(".cm-tooltip-arrow") : null
+ let arrowHeight = arrow ? Arrow.Size : 0
+ let width = size.right - size.left, height = size.bottom - size.top
+ let offset = tView.offset || noOffset, ltr = this.view.textDirection == Direction.LTR
+ let left = size.width > space.right - space.left ? (ltr ? space.left : space.right - size.width)
+ : ltr ? Math.min(pos.left - (arrow ? Arrow.Offset : 0) + offset.x, space.right - width)
+ : Math.max(space.left, pos.left - width + (arrow ? Arrow.Offset : 0) - offset.x)
+ let above = !!tooltip.above
+ if (!tooltip.strictSide && (above
+ ? pos.top - (size.bottom - size.top) - offset.y < space.top
+ : pos.bottom + (size.bottom - size.top) + offset.y > space.bottom) &&
+ above == (space.bottom - pos.bottom > pos.top - space.top))
+ above = !above
+ let top = above ? pos.top - height - arrowHeight - offset.y : pos.bottom + arrowHeight + offset.y
+ let right = left + width
+ if (tView.overlap !== true) for (let r of others)
+ if (r.left < right && r.right > left && r.top < top + height && r.bottom > top)
+ top = above ? r.top - height - 2 - arrowHeight : r.bottom + arrowHeight + 2
+ if (this.position == "absolute") {
+ dom.style.top = (top - measured.parent.top) + "px"
+ dom.style.left = (left - measured.parent.left) + "px"
+ } else {
+ dom.style.top = top + "px"
+ dom.style.left = left + "px"
+ }
+ if (arrow) arrow.style.left = `${pos.left + (ltr ? offset.x : -offset.x) - (left + Arrow.Offset - Arrow.Size)}px`
+
+ if (tView.overlap !== true)
+ others.push({left, top, right, bottom: top + height})
+ dom.classList.toggle("cm-tooltip-above", above)
+ dom.classList.toggle("cm-tooltip-below", !above)
+ if (tView.positioned) tView.positioned()
+ }
+ }
+
+ maybeMeasure() {
+ if (this.manager.tooltips.length) {
+ if (this.view.inView) this.view.requestMeasure(this.measureReq)
+ if (this.inView != this.view.inView) {
+ this.inView = this.view.inView
+ if (!this.inView) for (let tv of this.manager.tooltipViews) tv.dom.style.top = Outside
+ }
+ }
+ }
+}, {
+ eventHandlers: {
+ scroll() { this.maybeMeasure() }
+ }
+})
+
+const baseTheme = EditorView.baseTheme({
+ ".cm-tooltip": {
+ zIndex: 100
+ },
+ "&light .cm-tooltip": {
+ border: "1px solid #bbb",
+ backgroundColor: "#f5f5f5"
+ },
+ "&light .cm-tooltip-section:not(:first-child)": {
+ borderTop: "1px solid #bbb",
+ },
+ "&dark .cm-tooltip": {
+ backgroundColor: "#333338",
+ color: "white"
+ },
+ ".cm-tooltip-arrow": {
+ height: `${Arrow.Size}px`,
+ width: `${Arrow.Size * 2}px`,
+ position: "absolute",
+ zIndex: -1,
+ overflow: "hidden",
+ "&:before, &:after": {
+ content: "''",
+ position: "absolute",
+ width: 0,
+ height: 0,
+ borderLeft: `${Arrow.Size}px solid transparent`,
+ borderRight: `${Arrow.Size}px solid transparent`,
+ },
+ ".cm-tooltip-above &": {
+ bottom: `-${Arrow.Size}px`,
+ "&:before": {
+ borderTop: `${Arrow.Size}px solid #bbb`,
+ },
+ "&:after": {
+ borderTop: `${Arrow.Size}px solid #f5f5f5`,
+ bottom: "1px"
+ }
+ },
+ ".cm-tooltip-below &": {
+ top: `-${Arrow.Size}px`,
+ "&:before": {
+ borderBottom: `${Arrow.Size}px solid #bbb`,
+ },
+ "&:after": {
+ borderBottom: `${Arrow.Size}px solid #f5f5f5`,
+ top: "1px"
+ }
+ },
+ },
+ "&dark .cm-tooltip .cm-tooltip-arrow": {
+ "&:before": {
+ borderTopColor: "#333338",
+ borderBottomColor: "#333338"
+ },
+ "&:after": {
+ borderTopColor: "transparent",
+ borderBottomColor: "transparent"
+ }
+ }
+})
+
+/// Describes a tooltip. Values of this type, when provided through
+/// the [`showTooltip`](#tooltip.showTooltip) facet, control the
+/// individual tooltips on the editor.
+export interface Tooltip {
+ /// The document position at which to show the tooltip.
+ pos: number
+ /// The end of the range annotated by this tooltip, if different
+ /// from `pos`.
+ end?: number
+ /// A constructor function that creates the tooltip's [DOM
+ /// representation](#tooltip.TooltipView).
+ create(view: EditorView): TooltipView
+ /// Whether the tooltip should be shown above or below the target
+ /// position. Not guaranteed for hover tooltips since all hover
+ /// tooltips for the same range are always positioned together.
+ /// Defaults to false.
+ above?: boolean
+ /// Whether the `above` option should be honored when there isn't
+ /// enough space on that side to show the tooltip inside the
+ /// viewport. Not guaranteed for hover tooltips. Defaults to false.
+ strictSide?: boolean,
+ /// When set to true, show a triangle connecting the tooltip element
+ /// to position `pos`.
+ arrow?: boolean
+}
+
+/// Describes the way a tooltip is displayed.
+export interface TooltipView {
+ /// The DOM element to position over the editor.
+ dom: HTMLElement
+ /// Adjust the position of the tooltip relative to its anchor
+ /// position. A positive `x` value will move the tooltip
+ /// horizontally along with the text direction (so right in
+ /// left-to-right context, left in right-to-left). A positive `y`
+ /// will move the tooltip up when it is above its anchor, and down
+ /// otherwise.
+ offset?: {x: number, y: number}
+ /// By default, a tooltip's screen position will be based on the
+ /// text position of its `pos` property. This method can be provided
+ /// to make the tooltip view itself responsible for finding its
+ /// screen position.
+ getCoords?: (pos: number) => Rect
+ /// By default, tooltips are moved when they overlap with other
+ /// tooltips. Set this to `true` to disable that behavior for this
+ /// tooltip.
+ overlap?: boolean
+ /// Called after the tooltip is added to the DOM for the first time.
+ mount?(view: EditorView): void
+ /// Update the DOM element for a change in the view's state.
+ update?(update: ViewUpdate): void
+ /// Called when the tooltip has been (re)positioned.
+ positioned?(): void,
+}
+
+const noOffset = {x: 0, y: 0}
+
+/// Behavior by which an extension can provide a tooltip to be shown.
+export const showTooltip = Facet.define({
+ enables: [tooltipPlugin, baseTheme]
+})
+
+const showHoverTooltip = Facet.define()
+
+class HoverTooltipHost implements TooltipView {
+ private readonly manager: TooltipViewManager
+ dom: HTMLElement
+ mounted: boolean = false
+
+ // Needs to be static so that host tooltip instances always match
+ static create(view: EditorView) {
+ return new HoverTooltipHost(view)
+ }
+
+ private constructor(readonly view: EditorView) {
+ this.dom = document.createElement("div")
+ this.dom.classList.add("cm-tooltip-hover")
+ this.manager = new TooltipViewManager(view, showHoverTooltip, t => this.createHostedView(t))
+ }
+
+ createHostedView(tooltip: Tooltip) {
+ let hostedView = tooltip.create(this.view)
+ hostedView.dom.classList.add("cm-tooltip-section")
+ this.dom.appendChild(hostedView.dom)
+ if (this.mounted && hostedView.mount)
+ hostedView.mount(this.view)
+ return hostedView
+ }
+
+ mount(view: EditorView) {
+ for (let hostedView of this.manager.tooltipViews) {
+ if (hostedView.mount) hostedView.mount(view)
+ }
+ this.mounted = true
+ }
+
+ positioned() {
+ for (let hostedView of this.manager.tooltipViews) {
+ if (hostedView.positioned) hostedView.positioned()
+ }
+ }
+
+ update(update: ViewUpdate) {
+ this.manager.update(update)
+ }
+}
+
+const showHoverTooltipHost = showTooltip.compute([showHoverTooltip], state => {
+ let tooltips = state.facet(showHoverTooltip).filter(t => t) as Tooltip[]
+ if (tooltips.length === 0) return null
+
+ return {
+ pos: Math.min(...tooltips.map(t => t.pos)),
+ end: Math.max(...tooltips.filter(t => t.end != null).map(t => t.end!)),
+ create: HoverTooltipHost.create,
+ above: tooltips[0].above,
+ arrow: tooltips.some(t => t.arrow),
+ }
+})
+
+const enum Hover { Time = 300, MaxDist = 6 }
+
+class HoverPlugin {
+ lastMove: {x: number, y: number, target: HTMLElement, time: number}
+ hoverTimeout = -1
+ restartTimeout = -1
+ pending: {pos: number} | null = null
+
+ constructor(readonly view: EditorView,
+ readonly source: (view: EditorView, pos: number, side: -1 | 1) => Tooltip | null | Promise,
+ readonly field: StateField,
+ readonly setHover: StateEffectType,
+ readonly hoverTime: number) {
+ this.lastMove = {x: 0, y: 0, target: view.dom, time: 0}
+ this.checkHover = this.checkHover.bind(this)
+ view.dom.addEventListener("mouseleave", this.mouseleave = this.mouseleave.bind(this))
+ view.dom.addEventListener("mousemove", this.mousemove = this.mousemove.bind(this))
+ }
+
+ update() {
+ if (this.pending) {
+ this.pending = null
+ clearTimeout(this.restartTimeout)
+ //@ts-ignore
+ this.restartTimeout = setTimeout(() => this.startHover(), 20)
+ }
+ }
+
+ get active() {
+ return this.view.state.field(this.field)
+ }
+
+ checkHover() {
+ this.hoverTimeout = -1
+ if (this.active) return
+ let hovered = Date.now() - this.lastMove.time
+ if (hovered < this.hoverTime)
+ //@ts-ignore
+ this.hoverTimeout = setTimeout(this.checkHover, this.hoverTime - hovered)
+ else
+ this.startHover()
+ }
+
+ startHover() {
+ clearTimeout(this.restartTimeout)
+ let {lastMove} = this
+ let pos = this.view.contentDOM.contains(lastMove.target) ? this.view.posAtCoords(lastMove) : null
+ if (pos == null) return
+ let posCoords = this.view.coordsAtPos(pos)
+ if (posCoords == null || lastMove.y < posCoords.top || lastMove.y > posCoords.bottom ||
+ lastMove.x < posCoords.left - this.view.defaultCharacterWidth ||
+ lastMove.x > posCoords.right + this.view.defaultCharacterWidth) return
+ let bidi = this.view.bidiSpans(this.view.state.doc.lineAt(pos)).find(s => s.from <= pos! && s.to >= pos!)
+ let rtl = bidi && bidi.dir == Direction.RTL ? -1 : 1
+ let open = this.source(this.view, pos, (lastMove.x < posCoords.left ? -rtl : rtl) as -1 | 1)
+ if ((open as any)?.then) {
+ let pending = this.pending = {pos}
+ ;(open as Promise).then(result => {
+ if (this.pending == pending) {
+ this.pending = null
+ if (result) this.view.dispatch({effects: this.setHover.of(result)})
+ }
+ }, e => logException(this.view.state, e, "hover tooltip"))
+ } else if (open) {
+ this.view.dispatch({effects: this.setHover.of(open as Tooltip)})
+ }
+ }
+
+ mousemove(event: MouseEvent) {
+ this.lastMove = {x: event.clientX, y: event.clientY, target: event.target as HTMLElement, time: Date.now()}
+ //@ts-ignore
+ if (this.hoverTimeout < 0) this.hoverTimeout = setTimeout(this.checkHover, this.hoverTime)
+ let tooltip = this.active
+ if (tooltip && !isInTooltip(this.lastMove.target) || this.pending) {
+ let {pos} = tooltip || this.pending!, end = tooltip?.end ?? pos
+ if ((pos == end ? this.view.posAtCoords(this.lastMove) != pos
+ : !isOverRange(this.view, pos, end, event.clientX, event.clientY, Hover.MaxDist))) {
+ this.view.dispatch({effects: this.setHover.of(null)})
+ this.pending = null
+ }
+ }
+ }
+
+ mouseleave(e: MouseEvent) {
+ clearTimeout(this.hoverTimeout)
+ this.hoverTimeout = -1
+ // return;
+ if (this.active && !isInTooltip(e.relatedTarget as HTMLElement))
+ this.view.dispatch({effects: this.setHover.of(null)})
+ }
+
+ destroy() {
+ clearTimeout(this.hoverTimeout)
+ this.view.dom.removeEventListener("mouseleave", this.mouseleave)
+ this.view.dom.removeEventListener("mousemove", this.mousemove)
+ }
+}
+
+function isInTooltip(elt: HTMLElement) {
+ for (let cur: Node | null = elt; cur; cur = cur.parentNode)
+ if (cur.nodeType == 1 && (cur as HTMLElement).classList.contains("cm-tooltip")) return true
+ return false
+}
+
+function isOverRange(view: EditorView, from: number, to: number, x: number, y: number, margin: number) {
+ let range = document.createRange()
+ let fromDOM = view.domAtPos(from), toDOM = view.domAtPos(to)
+ range.setEnd(toDOM.node, toDOM.offset)
+ range.setStart(fromDOM.node, fromDOM.offset)
+ let rects = range.getClientRects()
+ range.detach()
+ for (let i = 0; i < rects.length; i++) {
+ let rect = rects[i]
+ let dist = Math.max(rect.top - y, y - rect.bottom, rect.left - x, x - rect.right)
+ if (dist <= margin) return true
+ }
+ return false
+}
+
+/// Enable a hover tooltip, which shows up when the pointer hovers
+/// over ranges of text. The callback is called when the mouse hovers
+/// over the document text. It should, if there is a tooltip
+/// associated with position `pos` return the tooltip description
+/// (either directly or in a promise). The `side` argument indicates
+/// on which side of the position the pointer is—it will be -1 if the
+/// pointer is before the position, 1 if after the position.
+///
+/// Note that all hover tooltips are hosted within a single tooltip
+/// container element. This allows multiple tooltips over the same
+/// range to be "merged" together without overlapping.
+export function hoverTooltip(
+ source: (view: EditorView, pos: number, side: -1 | 1) => Tooltip | null | Promise,
+ options: {
+ /// When enabled (this defaults to false), close the tooltip
+ /// whenever the document changes.
+ hideOnChange?: boolean,
+ /// Hover time after which the tooltip should appear, in
+ /// milliseconds. Defaults to 300ms.
+ hoverTime?: number
+ } = {}
+): Extension {
+ let setHover = StateEffect.define()
+ let hoverState = StateField.define({
+ create() { return null },
+
+ update(value, tr) {
+
+ if (value && (options.hideOnChange && (tr.docChanged || tr.selection))) return null
+ for (let effect of tr.effects) {
+ if (effect.is(setHover))
+ {
+ return effect.value
+ }
+ if (effect.is(closeHoverTooltipEffect)) return null
+ }
+ if (value && tr.docChanged) {
+ let newPos = tr.changes.mapPos(value.pos, -1, MapMode.TrackDel)
+ if (newPos == null) return null
+ let copy: Tooltip = Object.assign(Object.create(null), value)
+ copy.pos = newPos
+ if (value.end != null) copy.end = tr.changes.mapPos(value.end)
+ return copy
+ }
+ return value
+ },
+
+ provide: f => showHoverTooltip.from(f)
+ })
+
+ return [
+ hoverState,
+ ViewPlugin.define(view => new HoverPlugin(view, source, hoverState, setHover, options.hoverTime || Hover.Time)),
+ showHoverTooltipHost
+ ]
+}
+
+/// Get the active tooltip view for a given tooltip, if available.
+export function getTooltip(view: EditorView, tooltip: Tooltip): TooltipView | null {
+ let plugin = view.plugin(tooltipPlugin)
+ if (!plugin) return null
+ let found = plugin.manager.tooltips.indexOf(tooltip)
+ return found < 0 ? null : plugin.manager.tooltipViews[found]
+}
+
+/// Returns true if any hover tooltips are currently active.
+export function hasHoverTooltips(state: EditorState) {
+ return state.facet(showHoverTooltip).some(x => x)
+}
+
+const closeHoverTooltipEffect = StateEffect.define()
+
+/// Transaction effect that closes all hover tooltips.
+export const closeHoverTooltips = closeHoverTooltipEffect.of(null)
+
+/// Tell the tooltip extension to recompute the position of the active
+/// tooltips. This can be useful when something happens (such as a
+/// re-positioning or CSS change affecting the editor) that could
+/// invalidate the existing tooltip positions.
+export function repositionTooltips(view: EditorView) {
+ view.plugin(tooltipPlugin)?.maybeMeasure()
+}
\ No newline at end of file
diff --git a/src/components/FlowEditor/FlowEditor.tsx b/src/components/FlowEditor/FlowEditor.tsx
new file mode 100644
index 0000000..d1618dd
--- /dev/null
+++ b/src/components/FlowEditor/FlowEditor.tsx
@@ -0,0 +1,459 @@
+
+import MakeMDPlugin from "main";
+import {
+ Component,
+ MarkdownEditView,
+ OpenViewState,
+ parseLinktext,
+ requireApiVersion,
+ resolveSubpath,
+ setIcon,
+ TFile,
+ View,
+ Workspace,
+ WorkspaceLeaf,
+ WorkspaceSplit,
+ MarkdownView,
+ WorkspaceTabs,
+ Loc,
+ HoverPopover,
+ PopoverState,
+ MousePos,
+ EphemeralState,
+} from "obsidian";
+
+export function genId(size: number) {
+ const chars = [];
+ for (let n = 0; n < size; n++) chars.push(((16 * Math.random()) | 0).toString(16));
+ return chars.join("");
+ }
+
+
+import 'css/FlowEditor.css'
+
+export interface FlowEditorParent {
+ flowEditor: FlowEditor | null;
+ containerEl?: HTMLElement;
+ view?: View;
+ dom?: HTMLElement;
+ }
+const popovers = new WeakMap();
+type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit;
+
+let mouseCoords: MousePos = { x: 0, y: 0 };
+
+function nosuper(base: new (...args: unknown[]) => T): new () => T {
+ const derived = function () {
+ return Object.setPrototypeOf(new Component, new.target.prototype);
+ };
+ derived.prototype = base.prototype;
+ return Object.setPrototypeOf(derived, base);
+}
+
+
+export class FlowEditor extends nosuper(HoverPopover) {
+ onTarget: boolean;
+ setActive: (event: MouseEvent) => void;
+ shownPos: MousePos | null;
+
+
+ lockedOut: boolean;
+
+ abortController? = this.addChild(new Component());
+
+ detaching = false;
+
+ opening = false;
+
+ rootSplit: WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(window.app.workspace, "vertical");
+
+ targetRect = this.targetEl?.getBoundingClientRect();
+
+ pinEl: HTMLElement;
+
+ titleEl: HTMLElement;
+
+ containerEl: HTMLElement;
+
+ hideNavBarEl: HTMLElement;
+
+
+
+ oldPopover = this.parent?.flowEditor;
+
+ document: Document = this.targetEl?.ownerDocument ?? window.activeDocument ?? window.document;
+
+ id = genId(8);
+
+ bounce?: NodeJS.Timeout;
+
+ boundOnZoomOut: () => void;
+
+ originalPath: string; // these are kept to avoid adopting targets w/a different link
+ originalLinkText: string;
+
+ static activePopover?: FlowEditor;
+
+ static activeWindows() {
+ const windows: Window[] = [window];
+ const { floatingSplit } = app.workspace;
+ if (floatingSplit) {
+ for (const split of floatingSplit.children) {
+ if (split.win) windows.push(split.win);
+ }
+ }
+ return windows;
+ }
+
+ static containerForDocument(doc: Document) {
+ if (doc !== document && app.workspace.floatingSplit)
+ for (const container of app.workspace.floatingSplit.children) {
+ if (container.doc === doc) return container;
+ }
+ return app.workspace.rootSplit;
+ }
+
+ static activePopovers() {
+ //@ts-ignore
+ return this.activeWindows().flatMap(this.popoversForWindow);
+ }
+
+ static popoversForWindow(win?: Window) {
+ return (Array.prototype.slice.call(win?.document?.body.querySelectorAll(".mk-hover-popover") ?? []) as HTMLElement[])
+ .map(el => popovers.get(el)!)
+ .filter(he => he);
+ }
+
+ static forLeaf(leaf: WorkspaceLeaf | undefined) {
+ // leaf can be null such as when right clicking on an internal link
+ //@ts-ignore
+ const el = leaf && document.body.matchParent.call(leaf.containerEl, ".mk-hover-popover"); // work around matchParent race condition
+ return el ? popovers.get(el) : undefined;
+ }
+
+ hoverEl: HTMLElement = this.document.defaultView!.createDiv({
+ cls: "mk-floweditor mk-hover-popover",
+ attr: { id: "he" + this.id },
+ });
+
+ constructor(
+ parent: FlowEditorParent,
+ public targetEl: HTMLElement,
+ public plugin: MakeMDPlugin,
+ waitTime?: number,
+ public onShowCallback?: () => unknown,
+ ) {
+ //
+ super();
+
+ if (waitTime === undefined) {
+ waitTime = 300;
+ }
+ this.onTarget = true;
+
+ this.parent = parent;
+ this.waitTime = waitTime;
+ //@ts-ignore
+ this.state = PopoverState.Showing;
+ const { hoverEl } = this;
+
+ this.abortController!.load();
+ this.timer = window.setTimeout(this.show.bind(this), waitTime);
+ this.setActive = this._setActive.bind(this);
+ if (hoverEl) {
+ hoverEl.addEventListener("mousedown", this.setActive);
+ }
+ // custom logic begin
+ popovers.set(this.hoverEl, this);
+ this.hoverEl.addClass("hover-editor");
+ this.containerEl = this.hoverEl.createDiv("popover-content");
+ this.setTitleBar();
+ this.hoverEl.style.height = 'auto';
+ this.hoverEl.style.width = "100%";
+
+ }
+
+ _setActive() {
+ this.plugin.app.workspace.setActiveLeaf(this.leaves()[0], {focus: true})
+
+ }
+
+ getDefaultMode() {
+ //@ts-ignore
+ return this.parent?.view?.getMode ? this.parent.view.getMode() : "preview";
+ }
+
+ updateLeaves() {
+ if (this.onTarget && this.targetEl && !this.document.contains(this.targetEl)) {
+ this.onTarget = false;
+ this.transition();
+ }
+ let leafCount = 0;
+ this.plugin.app.workspace.iterateLeaves(leaf => {
+ leafCount++;
+ }, this.rootSplit);
+ if (leafCount === 0) {
+ this.hide(); // close if we have no leaves
+ } else if (leafCount > 1) {
+ }
+ this.hoverEl.setAttribute("data-leaf-count", leafCount.toString());
+ }
+
+
+ setTitleBar() {
+ this.titleEl = this.document.defaultView!.createDiv("mk-flow-titlebar");
+ this.containerEl.prepend(this.titleEl);
+
+ }
+
+ attachLeaf(): WorkspaceLeaf {
+ //@ts-ignore
+ this.rootSplit.getRoot = () => this.plugin.app.workspace[this.document === document ? "rootSplit" : "floatingSplit"]!;
+ this.rootSplit.getContainer = () => FlowEditor.containerForDocument(this.document);
+
+ this.titleEl.insertAdjacentElement("afterend", this.rootSplit.containerEl);
+ const leaf = this.plugin.app.workspace.createLeafInParent(this.rootSplit, 0);
+ this.updateLeaves();
+ return leaf;
+ }
+
+ onload(): void {
+ super.onload();
+ this.registerEvent(this.plugin.app.workspace.on("layout-change", this.updateLeaves, this));
+ this.registerEvent(app.workspace.on("layout-change", () => {
+ // Ensure that top-level items in a popover are not tabbed
+ //@ts-ignore
+ this.rootSplit.children.forEach((item, index) => {
+ if (item instanceof WorkspaceTabs) {
+ //@ts-ignore
+ this.rootSplit.replaceChild(index, item.children[0]);
+ }
+ })
+ }));
+ }
+
+ leaves() {
+ const leaves: WorkspaceLeaf[] = [];
+ this.plugin.app.workspace.iterateLeaves(leaf => {
+ leaves.push(leaf);
+ }, this.rootSplit);
+ return leaves;
+ }
+
+
+ onShow() {
+ const closeDelay = 600;
+ setTimeout(() => (this.waitTime = closeDelay), closeDelay);
+
+ this.oldPopover?.hide();
+ this.oldPopover = null;
+
+ this.hoverEl.toggleClass("is-new", true);
+
+ this.document.body.addEventListener(
+ "click",
+ () => {
+ this.hoverEl.toggleClass("is-new", false);
+ },
+ { once: true, capture: true },
+ );
+
+ if (this.parent) {
+ this.parent.flowEditor = this;
+ }
+ const viewHeaderEl = this.hoverEl.querySelector(".view-header");
+ viewHeaderEl?.remove();
+
+ const sizer = this.hoverEl.querySelector(".workspace-leaf");
+ this.hoverEl.appendChild(sizer);
+ const inlineTitle = this.hoverEl.querySelector(".inline-title");
+ inlineTitle.remove();
+ this.onShowCallback?.();
+ this.onShowCallback = undefined; // only call it once
+ }
+
+
+ transition() {
+ if (this.shouldShow()) {
+ //@ts-ignore
+ if (this.state === PopoverState.Hiding) {
+ //@ts-ignore
+ this.state = PopoverState.Shown;
+ clearTimeout(this.timer);
+ }
+ } else {
+ //@ts-ignore
+ if (this.state === PopoverState.Showing) {
+ this.hide();
+ } else {
+ //@ts-ignore
+ if (this.state === PopoverState.Shown) {
+ //@ts-ignore
+ this.state = PopoverState.Hiding;
+ this.timer = window.setTimeout(() => {
+ if (this.shouldShow()) {
+ this.transition();
+ } else {
+ this.hide();
+ }
+ }, this.waitTime);
+ }
+ }
+ }
+ }
+
+ shouldShow() {
+ return this.shouldShowSelf() || this.shouldShowChild();
+ }
+
+ shouldShowChild(): boolean {
+ //@ts-ignore
+ return FlowEditor.activePopovers().some(popover => {
+ if (popover !== this && popover.targetEl && this.hoverEl.contains(popover.targetEl)) {
+ return popover.shouldShow();
+ }
+ return false;
+ });
+ }
+
+ shouldShowSelf() {
+ return (
+ !this.detaching &&
+ !!(
+ this.onTarget ||
+ //@ts-ignore
+ (this.state == PopoverState.Shown) ||
+ this.document.querySelector(`body>.modal-container, body > #he${this.id} ~ .menu, body > #he${this.id} ~ .suggestion-container`)
+ )
+ );
+ }
+
+ show() {
+ if (!this.targetEl || this.document.body.contains(this.targetEl)) {
+ //@ts-ignore
+ this.state = PopoverState.Shown;
+ this.timer = 0;
+ this.shownPos = mouseCoords;
+ this.targetEl.replaceChildren(this.hoverEl);
+ this.onShow();
+ app.workspace.onLayoutChange();
+ this.load();
+ } else {
+ this.hide();
+ }
+ }
+
+ onHide() {
+ this.oldPopover = null;
+ if (this.parent?.flowEditor === this) {
+ this.parent.flowEditor = null;
+ }
+ }
+
+ hide() {
+ this.onTarget = false;
+ this.detaching = true;
+ // Once we reach this point, we're committed to closing
+
+ // in case we didn't ever call show()
+
+
+ // A timer might be pending to call show() for the first time, make sure
+ // it doesn't bring us back up after we close
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = 0;
+ }
+
+ // Hide our HTML element immediately, even if our leaves might not be
+ // detachable yet. This makes things more responsive and improves the
+ // odds of not showing an empty popup that's just going to disappear
+ // momentarily.
+ this.hoverEl.hide();
+
+ // If a file load is in progress, we need to wait until it's finished before
+ // detaching leaves. Because we set .detaching, The in-progress openFile()
+ // will call us again when it finishes.
+ if (this.opening) return;
+
+ const leaves = this.leaves();
+ if (leaves.length) {
+ // Detach all leaves before we unload the popover and remove it from the DOM.
+ // Each leaf.detach() will trigger layout-changed and the updateLeaves()
+ // method will then call hide() again when the last one is gone.
+ leaves.forEach(leaf => leaf.detach());
+ } else {
+ this.parent = null;
+ this.abortController?.unload();
+ this.abortController = undefined;
+ return this.nativeHide();
+ }
+ }
+
+ nativeHide() {
+ const { hoverEl, targetEl } = this;
+//@ts-ignore
+ this.state = PopoverState.Hidden;
+
+ hoverEl.detach();
+
+ if (targetEl) {
+ const parent = targetEl.matchParent(".mk-hover-popover");
+ if (parent) popovers.get(parent)?.transition();
+ }
+
+ this.onHide();
+ this.unload();
+ }
+
+ resolveLink(linkText: string, sourcePath: string): TFile | null {
+ const link = parseLinktext(linkText);
+ const tFile = link ? this.plugin.app.metadataCache.getFirstLinkpathDest(link.path, sourcePath) : null;
+ return tFile;
+ }
+
+
+ async openFile(file: TFile, openState?: OpenViewState, useLeaf?: WorkspaceLeaf) {
+ if (this.detaching) return;
+ const leaf = useLeaf ?? this.attachLeaf();
+ this.opening = true;
+ try {
+ await leaf.openFile(file, openState);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.opening = false;
+ if (this.detaching) this.hide();
+ }
+ this.plugin.app.workspace.setActiveLeaf(leaf);
+
+ return leaf;
+ }
+
+ buildState(parentMode: string, eState?: EphemeralState) {
+ return {
+ active: false,
+ state: { },
+ eState: eState,
+ };
+ }
+
+ buildEphemeralState(
+ file: TFile,
+ link?: {
+ path: string;
+ subpath: string;
+ },
+ ) {
+ const cache = this.plugin.app.metadataCache.getFileCache(file);
+ const subpath = cache ? resolveSubpath(cache, link?.subpath || "") : undefined;
+ const eState: EphemeralState = { subpath: link?.subpath };
+ if (subpath) {
+ eState.line = subpath.start.line;
+ eState.startLoc = subpath.start;
+ eState.endLoc = subpath.end || undefined;
+ }
+ return eState;
+ }
+}
+
diff --git a/src/components/FlowEditor/FlowEditorHover.tsx b/src/components/FlowEditor/FlowEditorHover.tsx
new file mode 100644
index 0000000..38960d0
--- /dev/null
+++ b/src/components/FlowEditor/FlowEditorHover.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+import t from 'i18n'
+import { uiIconSet } from 'utils/icons'
+export const FlowEditorHover = (props: {toggle: boolean, toggleState: boolean, toggleFlow: (e: React.MouseEvent) => void, openLink: (e: React.MouseEvent) => void}) => {
+ return <>
+ { props.toggle &&
+
+
}
+
+
+ >
+}
\ No newline at end of file
diff --git a/src/components/FlowView/FileRow.tsx b/src/components/FlowView/FileRow.tsx
new file mode 100644
index 0000000..5e02604
--- /dev/null
+++ b/src/components/FlowView/FileRow.tsx
@@ -0,0 +1,44 @@
+import dayjs from 'dayjs';
+import * as relativeTime from 'dayjs/plugin/relativeTime'
+import MakeMDPlugin from 'main';
+import { TAbstractFile, TFile, TFolder } from 'obsidian';
+import React, { useEffect, useState } from 'react'
+import { openFile, unifiedToNative } from 'utils/utils';
+import t from 'i18n'
+import { FolderObject } from './FlowComponent';
+import {uiIconSet} from 'utils/icons'
+interface FileRowProps {
+ item: FolderObject;
+ plugin: MakeMDPlugin
+}
+dayjs.extend(require('dayjs/plugin/relativeTime'))
+
+export const FileRow = (props: FileRowProps) => {
+ const [fileCache, setFileCache] = useState('');
+ useEffect(() => {
+ const setFileC = async (file: TFile) => {
+ const fc = await props.plugin.app.vault.cachedRead(file);
+ setFileCache(fc)
+ }
+ if (props.item.file instanceof TFile)
+ setFileC(props.item.file);
+
+ if (props.item.file instanceof TFolder)
+ setFileCache(props.item.children.length + t.flowView.itemsCount)
+ })
+
+ const {item} = props
+
+ return
+
+ { item.icon && unifiedToNative(item.icon[1]) }
+ |
+
+ //@ts-ignore
+ openFile({ ...props.item.file, isFolder: item.type == 'folder' }, props.plugin, false)}>
+ {item.name}
+ {fileCache.length == 0 ? t.flowView.emptyDoc : fileCache}
+ |
+ {item.created && dayjs(item.created).fromNow()} |
+}
\ No newline at end of file
diff --git a/src/components/FlowView/FlowComponent.tsx b/src/components/FlowView/FlowComponent.tsx
new file mode 100644
index 0000000..b0c41ae
--- /dev/null
+++ b/src/components/FlowView/FlowComponent.tsx
@@ -0,0 +1,58 @@
+import dayjs from 'dayjs'
+import MakeMDPlugin from 'main'
+import { TAbstractFile, TFile, TFolder } from 'obsidian'
+import React, { useState } from 'react'
+import { openFile } from 'utils/utils'
+import { FileRow } from './FileRow'
+import { FlowRow } from './FlowRow'
+import 'css/FlowComponent.css'
+import t from 'i18n'
+interface FolderComponentProps {
+ folder: TFolder
+ plugin: MakeMDPlugin
+}
+
+export type FolderObject = {
+ children?: TAbstractFile[],
+ file: TAbstractFile,
+ type: string,
+ icon: string,
+ name: string,
+ path: string,
+ created?: number,
+}
+
+export const FolderComponent = (props: FolderComponentProps) => {
+
+ // @ts-ignore
+ const filteredNotes : FolderObject[] = props.folder.children.map(f => {
+ return {
+ //@ts-ignore
+ type: f.children ? 'folder' : f.extension,
+ file: f,
+ name: f.name,
+ path: f.path,
+ icon: props.plugin.settings.fileIcons.find(([path, icon]) => path == f.path),
+ //@ts-ignore
+ children: f.children,
+ //@ts-ignore
+ created: f.stat ? f.stat.ctime : undefined
+ }
+ })
+ return
+
+
+ {props.folder.name}
+
+
+
+ {filteredNotes.length > 0 ?
+
{filteredNotes.map((f, i) => )}
+ :
+
{t.flowView.emptyFolder}
+ }
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/components/FlowView/FlowRow.tsx b/src/components/FlowView/FlowRow.tsx
new file mode 100644
index 0000000..6df62d8
--- /dev/null
+++ b/src/components/FlowView/FlowRow.tsx
@@ -0,0 +1,55 @@
+import { FlowEditor, FlowEditorParent } from 'components/FlowEditor/FlowEditor';
+import dayjs from 'dayjs';
+import * as relativeTime from 'dayjs/plugin/relativeTime'
+import MakeMDPlugin from 'main';
+import { TAbstractFile, TFile, TFolder, WorkspaceLeaf } from 'obsidian';
+import React, { useEffect, useRef, useState } from 'react'
+import { openFile, unifiedToNative } from 'utils/utils';
+import { FolderObject } from './FlowComponent';
+import { spawnPortal } from 'utils/flowEditor';
+import { uiIconSet } from 'utils/icons';
+dayjs.extend(require('dayjs/plugin/relativeTime'))
+interface FileRowProps {
+ item: FolderObject;
+ plugin: MakeMDPlugin
+}
+
+export const FlowRow = (props: FileRowProps) => {
+ const ref = useRef(null);
+ const [flowOpen, setFlowOpen] = useState(false);
+ const loadFile = () => {
+ const file = props.item.file;
+ const div = ref.current;
+ const newLeaf = spawnPortal(props.plugin, div);
+ newLeaf.openFile(file as TFile);
+ }
+
+ const toggleFlow = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ const newState = !flowOpen
+ if (newState) {
+ loadFile();
+ } else {
+ ref.current.empty();
+ }
+ setFlowOpen(newState);
+ }
+ const {item} = props
+
+ return
+
+ //@ts-ignore
+ openFile({ ...props.item.file, isFolder: item.type == 'folder' }, props.plugin.app, false)}>
+
+ { item.icon && unifiedToNative(item.icon[1]) }
+
{item.name}
+
{item.created && dayjs(item.created).fromNow()}
+
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/components/FlowView/FlowView.tsx b/src/components/FlowView/FlowView.tsx
new file mode 100644
index 0000000..c1b0c6a
--- /dev/null
+++ b/src/components/FlowView/FlowView.tsx
@@ -0,0 +1,83 @@
+import { ItemView, TFolder, ViewStateResult, WorkspaceLeaf } from 'obsidian';
+import React, { cloneElement, useEffect, useRef } from 'react';
+import ReactDOM from 'react-dom';
+import { createRoot, Root } from 'react-dom/client'
+import MakeMDPlugin from '../../main';
+export const FOLDER_VIEW_TYPE = 'make-folder-view';
+export const ICON = 'sheets-in-box';
+import { FolderComponent } from './FlowComponent';
+
+
+export class FlowView extends ItemView {
+ plugin: MakeMDPlugin;
+ currentFolderPath: string;
+ navigation = true;
+ folder: TFolder;
+ root: Root;
+
+ constructor(leaf: WorkspaceLeaf, plugin: MakeMDPlugin) {
+ super(leaf);
+ this.plugin = plugin;
+ }
+
+ getViewType(): string {
+ return FOLDER_VIEW_TYPE;
+ }
+
+ getDisplayText(): string {
+ return this.folder?.name;
+ }
+
+
+
+ async onClose() {
+ this.destroy();
+ }
+
+ destroy() {
+ if (this.root)
+ this.root.unmount();
+ }
+
+ async onOpen(): Promise {
+ this.destroy();
+ }
+
+ async setState(state: any, result: ViewStateResult): Promise {
+
+ const folder = this.plugin.app.vault.getAbstractFileByPath(state.folder) as TFolder;
+
+ this.folder = folder;
+
+ this.constructFileTree(folder);
+ await super.setState(state, result);
+
+ this.leaf.tabHeaderInnerTitleEl.innerText = folder.name;
+ //@ts-ignore
+ this.leaf.view.titleEl = folder.name;
+ const headerEl = this.leaf.view.headerEl;
+ if (headerEl) {
+ //@ts-ignore
+ headerEl.querySelector('.view-header-title').innerText = folder.name
+ }
+
+ return;
+ }
+ getState(): any {
+ let state = super.getState();
+ state.folder = this.folder?.path;
+ // Store information to the state, whenever the workspace changes (opening a new note,...), the view's `getState` will be called, and the resulting state will be saved in the 'workspace' file
+
+ return state;
+ }
+
+ constructFileTree(folder: TFolder) {
+ this.destroy();
+ this.root = createRoot(this.contentEl);
+ this.root.render(
+
+
+
+ );
+ }
+}
diff --git a/src/components/MakeMenu/MakeMenu.tsx b/src/components/MakeMenu/MakeMenu.tsx
new file mode 100644
index 0000000..09fa524
--- /dev/null
+++ b/src/components/MakeMenu/MakeMenu.tsx
@@ -0,0 +1,106 @@
+
+import MakeMDPlugin from "main"
+import {
+ App,
+ Editor,
+ EditorPosition,
+ EditorSuggest,
+ EditorSuggestContext,
+ EditorSuggestTriggerInfo,
+ TFile,
+} from "obsidian"
+import 'css/MakeMenu.css'
+import { resolveCommands, Command } from "./commands"
+import t from "i18n"
+import { makeIconSet, markIconSet } from "utils/icons";
+
+export default class MakeMenu extends EditorSuggest {
+ inCmd = false
+ cmdStartCh = 0
+ plugin: MakeMDPlugin
+
+ constructor(app: App, plugin: MakeMDPlugin) {
+ super(app)
+ this.plugin = plugin
+
+ }
+ resetInfos() {
+ this.cmdStartCh = 0
+ this.inCmd = false
+ }
+
+ onTrigger(
+ cursor: EditorPosition,
+ editor: Editor,
+ _file: TFile
+ ): EditorSuggestTriggerInfo {
+ const currentLine = editor.getLine(cursor.line).slice(0, cursor.ch)
+
+ if (
+ !this.inCmd &&
+ currentLine[0] !==
+ this.plugin.settings.menuTriggerChar
+ ) {
+ this.resetInfos()
+ return null
+ }
+
+ if (!this.inCmd) {
+ this.cmdStartCh = currentLine.length - 1
+ this.inCmd = true
+ }
+
+ const currentCmd = currentLine.slice(this.cmdStartCh, cursor.ch)
+
+ if (
+ currentCmd.includes(" ") ||
+ !currentCmd.includes(this.plugin.settings.menuTriggerChar)
+ ) {
+ this.resetInfos()
+ return null
+ }
+ return { start: cursor, end: cursor, query: currentCmd.slice(1) }
+ }
+
+ getSuggestions(
+ context: EditorSuggestContext
+ ): Command[] | Promise {
+ const suggestions = resolveCommands(this.plugin).filter(({ label }) =>
+ //@ts-ignore
+ label.toLowerCase().includes(context.query.toLowerCase()) || (t.commands[label] && t.commands[label].toLowerCase().includes(context.query.toLowerCase()))
+ )
+
+ return suggestions.length > 0
+ ? suggestions
+ : [{ label: t.commandsSuggest.noResult, value: "", icon: ''}]
+ }
+
+ renderSuggestion(value: Command, el: HTMLElement): void {
+
+ const div = el.createDiv("mk-slash-item")
+ const icon = div.createDiv('mk-slash-icon');
+ icon.innerHTML = makeIconSet[value.icon];
+ const title = div.createDiv()
+ //@ts-ignore
+ title.setText(t.commands[value.label])
+
+ }
+
+ selectSuggestion(cmd: Command, _evt: MouseEvent | KeyboardEvent): void {
+ if (cmd.label === t.commandsSuggest.noResult) return
+
+ this.context.editor.replaceRange(
+ cmd.value,
+ { ...this.context.start, ch: this.cmdStartCh },
+ this.context.end
+ )
+ if (cmd.offset) {
+ this.context.editor.setSelection({ ...this.context.start, ch: cmd.offset[1] }, { ...this.context.end, ch: cmd.value.length+cmd.offset[0] })
+ }
+
+
+ this.resetInfos()
+
+ this.close()
+ }
+}
diff --git a/src/components/MakeMenu/commands/default.ts b/src/components/MakeMenu/commands/default.ts
new file mode 100644
index 0000000..df8f182
--- /dev/null
+++ b/src/components/MakeMenu/commands/default.ts
@@ -0,0 +1,93 @@
+import { Command } from ".";
+
+export default [
+ {
+ label: "todo",
+ value: "- [ ] ",
+ icon: 'mk-make-todo'
+ },
+ {
+ label: "list",
+ value: `- `,
+ icon: 'mk-make-list'
+ },
+ {
+ label: "ordered-list",
+ value: `1. `,
+ icon: 'mk-make-ordered'
+ },
+ {
+ label: "h1",
+ value: "# ",
+ icon: 'mk-make-h1'
+ },
+ {
+ label: "h2",
+ value: "## ",
+ icon: 'mk-make-h2'
+ },
+ {
+ label: "h3",
+ value: "### ",
+ icon: 'mk-make-h3'
+ },
+ {
+ label: "quote",
+ value: "> ",
+ icon: 'mk-make-quote'
+ },
+ {
+ label: "divider",
+ value: `
+---
+`,
+icon: 'mk-make-hr'
+ },
+ {
+ label: "link",
+ value: "",
+ offset: [-1, 1],
+ icon: 'mk-make-link'
+ },
+ {
+ label: "image",
+ value: "![](Paste Link)",
+ offset: [-1, 4],
+ icon: 'mk-make-image'
+ },
+ {
+ label: "codeblock",
+ value: `
+\`\`\`
+Type/Paste Your Code
+\`\`\``,
+ offset:[-3, 3],
+ icon: 'mk-make-codeblock'
+
+ },
+ {
+ label: "callout",
+ value: `> [!NOTE]
+> Content`,
+ offset: [-7, 12],
+ icon: 'mk-make-callout'
+ },
+ {
+ label: "note",
+ value: "[[Note Name]]",
+ offset: [-2, 2],
+ icon: 'mk-make-note'
+ },
+ {
+ label: "flow",
+ value: `!![[Note Name]]`,
+ offset: [-2, 4],
+ icon: 'mk-make-flow'
+ },
+ {
+ label: "tag",
+ value: "#tag",
+ offset: [0, 1],
+ icon: 'mk-make-tag'
+ },
+] as Command[]
diff --git a/src/components/MakeMenu/commands/index.ts b/src/components/MakeMenu/commands/index.ts
new file mode 100644
index 0000000..224775a
--- /dev/null
+++ b/src/components/MakeMenu/commands/index.ts
@@ -0,0 +1,14 @@
+import MakeMDPlugin from "main"
+import React from "react"
+import defaultCommands from "./default"
+
+export type Command = {
+ label: string
+ value: string
+ offset?: [number, number]
+ icon: string
+}
+
+export function resolveCommands(plugin: MakeMDPlugin) : Command[] {
+ return defaultCommands
+}
diff --git a/src/components/Spaces/FileExplorerVirtualized.tsx b/src/components/Spaces/FileExplorerVirtualized.tsx
new file mode 100644
index 0000000..72c2a5a
--- /dev/null
+++ b/src/components/Spaces/FileExplorerVirtualized.tsx
@@ -0,0 +1,456 @@
+import { FileTreeView } from "components/Spaces/FileTreeView";
+import React, { LegacyRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import MakeMDPlugin from 'main';
+import { CustomVaultChangeEvent, eventTypes, FlattenedTreeNode, FolderTree, SectionTree, VaultChange } from "types/types";
+import useForceUpdate from "hooks/ForceUpdate";
+import { Notice, Platform, TAbstractFile, TFile, TFolder } from "obsidian";
+import { useRecoilState } from "recoil";
+import * as recoilState from 'recoil/pluginState';
+import * as FileTreeUtils from 'utils/utils';
+import { IndicatorState, SortableTreeItem, SortableTreeItemProps, TreeItem } from "components/Spaces/TreeView/FolderTreeView";
+import { Active, closestCenter, CollisionDetection, DndContext as DndKitContext, DragEndEvent, DragMoveEvent, DragOverEvent, DragOverlay, DragStartEvent, getFirstCollision, MeasuringStrategy, Modifier, MouseSensor, Over, PointerSensor, pointerWithin, rectIntersection, TouchSensor, UniqueIdentifier, useDndMonitor, useSensor, useSensors } from "@dnd-kit/core";
+import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
+import { createPortal } from "react-dom";
+import { FOLDER_VIEW_TYPE } from "components/FlowView/FlowView";
+import t from 'i18n'
+import { VariableSizeList as List, areEqual, ListChildComponentProps, VariableSizeList } from "react-window";
+import AutoSizer from 'utils/autosizer'
+interface FileExplorerComponentProps {
+ fileTreeView: FileTreeView;
+ plugin: MakeMDPlugin;
+}
+
+
+const row: React.FC = memo(({data, index, style}) =>
+ {
+ const {flattenedItems, projected, handleCollapse, plugin, sections, openFolders, indentationWidth} = data;
+ const f = flattenedItems[index];
+ return i == f.id)}
+ onCollapse={
+ handleCollapse
+ }
+ >
+ }, areEqual
+ )
+
+export const FileExplorerComponent = (props: FileExplorerComponentProps) => {
+
+
+
+ const { plugin } = props;
+ const indentationWidth = 24;
+ const isMobile = FileTreeUtils.platformIsMobile();
+ const [vaultCollapsed, setVaultCollapsed] = useState(plugin.settings.vaultCollapsed);
+ const [openFolders, setOpenFolders] = useRecoilState(recoilState.openFolders);
+ const [fileIcons, setFileIcons] = useRecoilState(recoilState.fileIcons);
+ const [focusedFolder, setFocusedFolder] = useRecoilState(recoilState.focusedFolder);
+ const [activeFile, setActiveFile] = useRecoilState(recoilState.activeFile);
+ const [sections, setSections] = useRecoilState(recoilState.sections);
+ const [_folderTree, setFolderTree] = useRecoilState(recoilState.folderTree);
+ // const [dropPlaceholderItem, setDropPlaceholderItem] = useState<[Record, number] | null>(null);
+ const [offsetLeft, setOffsetLeft] = useState(0);
+
+ const listRef = useRef();
+ const forceUpdate = useForceUpdate();
+
+ // Persistant Settings
+ const loadFolderTree = async (folder: TFolder) => {
+ setFolderTree(await FileTreeUtils.sortFolderTree(folder, plugin));
+ }
+
+
+ useEffect(() => {
+ window.addEventListener(eventTypes.vaultChange, vaultChangeEvent);
+ window.addEventListener(eventTypes.activeFileChange, changeActiveFile);
+ window.addEventListener(eventTypes.refreshView, forceUpdate);
+ window.addEventListener(eventTypes.settingsChanged, settingsChanged);
+ // window.addEventListener(eventTypes.revealFile, handleRevealFileEvent);
+ return () => {
+ window.removeEventListener(eventTypes.vaultChange, vaultChangeEvent);
+ window.removeEventListener(eventTypes.activeFileChange, changeActiveFile);
+ window.removeEventListener(eventTypes.refreshView, forceUpdate);
+ window.removeEventListener(eventTypes.settingsChanged, settingsChanged);
+ // window.removeEventListener(eventTypes.revealFile, handleRevealFileEvent);
+ };
+ }, []);
+
+ const handleRevealFileEvent = (evt: CustomVaultChangeEvent) => {
+ // todo reveal file
+ }
+ const vaultChangeEvent = (evt: CustomVaultChangeEvent) => {
+
+ if (evt.detail) {
+ handleVaultChanges(evt.detail.file, evt.detail.changeType, evt.detail.oldPath);
+ }
+ const loadFolderTree = async () => {
+ setFolderTree(await FileTreeUtils.sortFolderTree(plugin.app.vault.getRoot(), plugin));
+ }
+ cleanData();
+ plugin.saveSettings();
+ loadFolderTree();
+ };
+
+ const changeActiveFile = (evt: CustomEvent) => {
+ let filePath: string = evt.detail.filePath;
+ const activeLeaf = plugin.app.workspace.activeLeaf
+ if (activeLeaf.view.getViewType() == FOLDER_VIEW_TYPE) {
+ setActiveFile(activeLeaf.view.getState().folder)
+ } else {
+ let file = plugin.app.vault.getAbstractFileByPath(filePath);
+ if (file) { setActiveFile(file.path) } else { setActiveFile(null) };
+ }
+
+ };
+
+ function handleVaultChanges(file: TAbstractFile, changeType: VaultChange, oldPathBeforeRename?: string) {
+ // Get Current States from Setters
+ if (changeType == 'rename') {
+ FileTreeUtils.renamePathInStringTree(oldPathBeforeRename, file, plugin);
+ }
+ if (changeType == 'delete') {
+
+ }
+
+ // File Event Handlers
+ }
+
+ const settingsChanged = () => {
+
+ setSections(plugin.settings.spaces);
+ setOpenFolders(plugin.settings.openFolders);
+ setFileIcons(plugin.settings.fileIcons)
+ setVaultCollapsed(plugin.settings.vaultCollapsed)
+ }
+
+ useEffect(() => {
+ setInitialFocusedFolder();
+ settingsChanged();
+
+ }, []);
+
+ const cleanData = () => {
+ const cleanedSections = plugin.settings.spaces.map(f => {return {
+ ...f,
+ children: f.children.filter(f => plugin.app.vault.getAbstractFileByPath(f))
+ }})
+
+ const cleanedCollapse = plugin.settings.openFolders
+ const cleanedFileIcons = plugin.settings.fileIcons.filter(f => plugin.app.vault.getAbstractFileByPath(f[0]))
+ plugin.settings.spaces = cleanedSections;
+ plugin.settings.openFolders = cleanedCollapse;
+ plugin.settings.fileIcons = cleanedFileIcons;
+ }
+ const setInitialFocusedFolder = () => {
+ cleanData();
+ loadFolderTree(plugin.app.vault.getRoot())
+ setFocusedFolder(plugin.app.vault.getRoot());
+ };
+ const sensors = useSensors(
+ useSensor(MouseSensor, {
+ activationConstraint: {
+ distance: 10
+ }
+ }),
+ useSensor(TouchSensor, {
+ activationConstraint: {
+ delay: 250,
+ tolerance: 5
+ },
+ })
+
+ );
+ const measuring = {
+ droppable: {
+ strategy: MeasuringStrategy.Always,
+ },
+ };
+ const [activeId, setActiveId] = useState(null);
+ const [overId, setOverId] = useState(null);
+ const [currentPosition, setCurrentPosition] = useState<{
+ parentId: UniqueIdentifier | null;
+ overId: UniqueIdentifier;
+ } | null>(null);
+
+
+
+
+
+ const flattenSectionTree = (sectionTrees: SectionTree[]) : FlattenedTreeNode[] => {
+ const getChildren = (section: string, paths: string[], sectionIndex: number) => {
+ return FileTreeUtils.flattenTrees(paths.map(f => plugin.app.vault.getAbstractFileByPath(f)).filter(f => f != null), '/'+section+'/', sectionIndex, section, 1)
+ }
+
+ return sectionTrees.reduce((p, c, i) => {
+ return [...p, {
+ id: c.section,
+ parentId: null,
+ name: c.section,
+ depth: 0,
+ index: i,
+ section: i,
+ isFolder: true,
+ }, ...!c.collapsed ? getChildren(c.section, c.children, i) : []]
+ }, [])
+ }
+
+ const flattenedItems = useMemo(() => {
+ const flattenedTree = [...flattenSectionTree(sections),
+ ..._folderTree ? FileTreeUtils.flattenTree(_folderTree, '/', -1, vaultCollapsed) : [],];
+ return FileTreeUtils.includeChildrenOf(flattenedTree, openFolders)
+
+ }, [_folderTree, openFolders, sections, vaultCollapsed]);
+ const sortedIds = useMemo(() => flattenedItems.map(({id}) => id), [flattenedItems]);
+
+ const activeItem = activeId
+ ? flattenedItems.find(({id}) => id === activeId)
+ : null;
+
+ const overIndex = overId
+ ? flattenedItems.findIndex(({id}) => id === overId)
+ : null;
+
+ const overItem = flattenedItems[overIndex];
+ const nextItem = flattenedItems[overIndex+1];
+
+ const dragDepth = useMemo(() => {
+ return FileTreeUtils.getDragDepth(offsetLeft, indentationWidth)}, [offsetLeft])
+
+ const projected = useMemo(() => {
+ return activeId && overId
+ ? FileTreeUtils.getProjection(
+ flattenedItems,
+ activeItem,
+ overIndex,
+ overItem,
+ nextItem,
+ dragDepth
+ )
+ : null;
+ }, [flattenedItems, activeItem, overItem, nextItem, overIndex, dragDepth])
+
+
+ function handleDragStart(event: DragStartEvent) {
+ const {active: {id: activeId}} = event;
+ const activeItem = flattenedItems.find(({id}) => id === activeId);
+ //Dont drag vault
+ if (activeItem.parentId == null && activeItem.section == -1)
+ return;
+ setActiveId(activeId);
+ setOverId(activeId);
+
+ if (activeItem) {
+ setCurrentPosition({
+ parentId: activeItem.parentId,
+ overId: activeId,
+ });
+ }
+
+ document.body.style.setProperty('cursor', 'grabbing');
+
+ }
+
+ function handleDragMove({delta}: DragMoveEvent) {
+
+ setOffsetLeft(Math.max(1, delta.x));
+ }
+
+ function handleDragOver({over}: DragOverEvent) {
+ const overId = over?.id;
+ if (overId) {
+ // if (!FileTreeUtils.nodeIsAncestorOfTarget(activeItem, flattenedItems.find(f => f.id == overId))) {
+ setOverId(over?.id ?? null);
+ // }
+ }
+ }
+
+ function handleDragEnd({active, over}: DragEndEvent) {
+ resetState();
+ moveFile(active, over)
+ }
+ const moveFile = async (active: Active, over: Over) => {
+ if (projected) {
+ const clonedItems: FlattenedTreeNode[] = [...flattenSectionTree(sections),
+ ..._folderTree ? FileTreeUtils.flattenTree(_folderTree, '/', -1, false) : [],]
+ const overIndex = clonedItems.findIndex(({id}) => id === over.id);
+ const overItem = clonedItems[overIndex];
+ const activeIndex = clonedItems.findIndex(({id}) => id === active.id);
+ const activeTreeItem = clonedItems[activeIndex];
+
+ const activeIsSection = activeTreeItem.parentId == null;
+ const overIsSection = overItem.parentId == null;
+ if (activeIsSection) {
+ if (overIsSection) {
+ const newSections = overItem.section == -1 ? arrayMove(sections, activeTreeItem.index, sections.length-1) : overItem.index > activeIndex ? arrayMove(sections, activeTreeItem.index, overItem.index-1) : arrayMove(sections, activeTreeItem.index, overItem.index)
+ plugin.settings.spaces = newSections;
+ plugin.saveSettings();
+ return;
+ }
+ }
+ const {depth, overId, parentId} = projected;
+ const parentItem = clonedItems.find(({id}) => id === parentId);
+
+ if (overItem.section != activeItem.section || overItem.section != -1) {
+ if (overItem.section == -1) {
+ return;
+ }
+ if (parentId != sections[overItem.section].section && parentId != null) {
+ return;
+ }
+
+ const newSections = sections.map((s,k) => {
+ if (k == overItem.section) {
+ const index = sections[overItem.section].children.findIndex(f => f == overItem.path) + 1;
+ const activeIndex = s.children.findIndex(g => g == activeItem.path);
+ const children = s.children.filter(g => g != activeItem.path);
+ const toIndex = activeIndex <= index && activeIndex != -1 ? index - 1 : index
+ if(activeIndex == -1) {
+ new Notice(t.notice.addedToSection)
+ }
+ return {
+ ...s,
+ children: [...children.slice(0, toIndex), activeItem.path, ...children.slice(toIndex)]
+ }
+ }
+ return s
+ })
+
+ plugin.settings.spaces = newSections;
+ plugin.saveSettings();
+ return;
+ }
+ if (parentId != activeTreeItem.parentId) {
+ const newPath = `${parentItem.isFolder ? parentItem.path : parentItem.parent.path}/${activeItem.name}`;
+ if (plugin.app.vault.getAbstractFileByPath(newPath)) {
+
+ new Notice(t.notice.duplicateFile)
+ return
+ }
+ await props.plugin.app.vault.rename(activeTreeItem, newPath);
+ clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId, ...plugin.app.vault.getAbstractFileByPath(newPath)} as FlattenedTreeNode;
+ } else {
+ clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId} as FlattenedTreeNode;
+ }
+
+
+
+ const sortedItems = overIndex > activeIndex ? arrayMove(clonedItems, activeIndex, overIndex) : overIndex < activeIndex ? arrayMove(clonedItems, activeIndex, overIndex+1) : clonedItems;
+ const newItems = FileTreeUtils.buildTree(sortedItems);
+ const newFolderRank = FileTreeUtils.folderTreeToStringTree(newItems);
+ plugin.settings.folderRank = newFolderRank
+
+ await plugin.saveSettings();
+ loadFolderTree(plugin.app.vault.getRoot())
+ }
+}
+ const adjustTranslate: Modifier = ({transform}) => {
+ return {
+ ...transform,
+ x: transform.x,
+ y: transform.y - 10,
+ };
+ };
+
+
+ function handleDragCancel() {
+ resetState();
+ }
+
+ const handleCollapse = useCallback((folder: FlattenedTreeNode) => {
+ if(folder.parentId == null) {
+ if (folder.id == '/') {
+
+ plugin.settings.vaultCollapsed = !plugin.settings.vaultCollapsed;
+ plugin.saveSettings();
+ return;
+ }
+ const newSections = sections.map((s, i) => {
+ return i == folder.index ? {...s, collapsed: !s.collapsed} : s
+ });
+ plugin.settings.spaces = newSections;
+ plugin.saveSettings();
+ } else {
+ const folderOpen = openFolders.find(f => f == folder.id);
+ const newOpenFolders : string[] = !folderOpen ? [...openFolders, folder.id] as string[] : openFolders.filter((openFolder) => folder.id !== openFolder) as string[];
+ plugin.settings.openFolders = newOpenFolders;
+ plugin.saveSettings();
+ }
+ }, [plugin, openFolders, sections])
+
+ function resetState() {
+ setOverId(null);
+ setActiveId(null);
+ setOffsetLeft(0);
+ // setDropPlaceholderItem(null);
+ document.body.style.setProperty('cursor', '');
+ }
+
+
+ const itemData = useMemo(() =>
+ {
+ if (listRef?.current)
+ listRef.current.resetAfterIndex(0);
+ return {flattenedItems, projected, handleCollapse, plugin, sections, openFolders, indentationWidth}
+ },
+ [flattenedItems, projected, handleCollapse, plugin, sections, openFolders, indentationWidth]
+ );
+
+ const rowHeight = (index: number) => isMobile ? flattenedItems[index].parentId == null ? 60 : 40 : flattenedItems[index].parentId == null ? 44 : 29;
+
+ return
+
+
+
+ {({ height, width}) =>
+
+ {row}
+
+ }
+
+ {createPortal(
+
+ {activeId ? (
+ f.id == activeId)}
+ indicator={null}
+ depth={0}
+ disabled={false}
+ plugin={plugin}
+ clone
+ childCount={0}
+ style={{}}
+ indentationWidth={indentationWidth}
+ />
+ ) : null}
+ ,
+ document.body
+ )}
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/components/Spaces/FileStickerMenu/FileStickerMenu.tsx b/src/components/Spaces/FileStickerMenu/FileStickerMenu.tsx
new file mode 100644
index 0000000..7e2df01
--- /dev/null
+++ b/src/components/Spaces/FileStickerMenu/FileStickerMenu.tsx
@@ -0,0 +1,45 @@
+import { EmojiData } from 'components/StickerMenu/emojis'
+import { emojis } from 'components/StickerMenu/emojis/default'
+import React, { useEffect, useRef, useState } from 'react'
+import { unifiedToNative } from 'utils/utils';
+import t from "i18n"
+import { App, FuzzyMatch, FuzzySuggestModal } from 'obsidian';
+
+
+export class StickerModal extends FuzzySuggestModal {
+
+ setIcon: (emoji: string) => void;
+ constructor(app: App, setIcon: (emoji: string) => void) {
+ super(app);
+ this.setIcon = setIcon;
+ this.resultContainerEl.toggleClass('mk-sticker-modal', true)
+ this.inputEl.focus();
+ this.emptyStateText = t.labels.findStickers
+ this.limit = 0;
+ }
+
+ renderSuggestion(item: FuzzyMatch, el: HTMLElement): void {
+ el.innerHTML = unifiedToNative(item.item.unicode);
+ el.setAttr('aria-label', item.item.label)
+ }
+
+ getItemText(item: Emoji): string {
+ return item.label+item.desc;
+ }
+
+ getItems(): Emoji[] {
+ const allEmojis : Emoji[] = Object.keys(emojis as EmojiData).reduce((p,c: string) => [...p, ...emojis[c].map(e => ({ label: e.n[0], desc: e.n[1], variants: e.v, unicode: e.u}))], [])
+ return allEmojis;
+ }
+
+ onChooseItem(item: Emoji, evt: MouseEvent | KeyboardEvent) {
+ this.setIcon(item.unicode)
+ }
+}
+
+interface Emoji {
+ label: string,
+ desc: string,
+ variants: string[],
+ unicode: string
+}
diff --git a/src/components/Spaces/FileTreeView.tsx b/src/components/Spaces/FileTreeView.tsx
new file mode 100644
index 0000000..93f84ff
--- /dev/null
+++ b/src/components/Spaces/FileTreeView.tsx
@@ -0,0 +1,82 @@
+import { ItemView, TAbstractFile, TFile, WorkspaceLeaf } from 'obsidian';
+import React, { cloneElement, useEffect, useRef } from 'react';
+import ReactDOM from 'react-dom';
+import { createRoot, Root } from 'react-dom/client'
+import MakeMDPlugin from '../../main';
+import { RecoilRoot } from 'recoil';
+import { FileExplorerComponent as VirtualizedFileExplorer } from 'components/Spaces/FileExplorerVirtualized';
+import { NewNotes } from 'components/Spaces/NewNote';
+import 'css/FileTree.css'
+export const FILE_TREE_VIEW_TYPE = 'mk-file-view';
+export const SETS_VIEW_TYPE = 'mk-sets-view';
+export const VIEW_DISPLAY_TEXT = 'Spaces';
+export const ICON = 'sheets-in-box';
+
+import { MainMenu } from 'components/Spaces/MainMenu';
+import { FOLDER_VIEW_TYPE } from 'components/FlowView/FlowView';
+import { platformIsMobile } from 'utils/utils';
+
+
+export class FileTreeView extends ItemView {
+ plugin: MakeMDPlugin;
+ currentFolderPath: string;
+ navigation = false;
+ root: Root;
+
+ constructor(leaf: WorkspaceLeaf, plugin: MakeMDPlugin) {
+ super(leaf);
+ this.plugin = plugin;
+ }
+
+ revealInFolder(folder: TAbstractFile) {
+ this.plugin.app.workspace.activeLeaf.setViewState({ type: FOLDER_VIEW_TYPE, state: { folder: folder.path }})
+ this.plugin.app.workspace.requestSaveLayout()
+
+ }
+ getViewType(): string {
+ return FILE_TREE_VIEW_TYPE;
+ }
+
+ getDisplayText(): string {
+ return VIEW_DISPLAY_TEXT;
+ }
+
+ getIcon(): string {
+ return ICON;
+ }
+
+ async onClose() {
+ let leafs = this.app.workspace.getLeavesOfType(FILE_TREE_VIEW_TYPE);
+ if (leafs.length == 0) {
+ let leaf = this.app.workspace.getLeftLeaf(false);
+ await leaf.setViewState({ type: FILE_TREE_VIEW_TYPE });
+ }
+ this.destroy();
+ }
+
+ destroy() {
+ if (this.root)
+ this.root.unmount();
+ }
+
+ async onOpen(): Promise {
+ this.destroy();
+ this.constructFileTree(this.app.vault.getRoot().path, '');
+ }
+
+
+ constructFileTree(folderPath: string, vaultChange: string) {
+ this.destroy();
+ this.root = createRoot(this.contentEl)
+ this.root.render(
+
+
+ {
+ !platformIsMobile() ? : null
+ }
+
+
+
+
)
+ }
+}
diff --git a/src/components/Spaces/MainMenu.tsx b/src/components/Spaces/MainMenu.tsx
new file mode 100644
index 0000000..efa5c32
--- /dev/null
+++ b/src/components/Spaces/MainMenu.tsx
@@ -0,0 +1,164 @@
+import MakeMDPlugin from "main";
+import 'css/MainMenu.css';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { SectionChangeModal } from "components/Spaces/modals";
+import t from "i18n"
+import { createRoot } from "react-dom/client";
+import { platformIsMobile } from "utils/utils";
+import { Menu, setIcon, WorkspaceLeaf, WorkspaceMobileDrawer } from "obsidian";
+import { FILE_TREE_VIEW_TYPE } from "./FileTreeView";
+import { uiIconSet } from "utils/icons";
+interface MainMenuComponentProps {
+ plugin: MakeMDPlugin;
+}
+export const replaceMobileMainMenu = (plugin: MakeMDPlugin) => {
+ if (platformIsMobile()) {
+ const header = app.workspace.containerEl.querySelector('.workspace-drawer.mod-left .workspace-drawer-header-left');
+ const reactEl = createRoot(header);
+ reactEl.render()
+ }
+
+}
+
+export const MainMenu = (props: MainMenuComponentProps) => {
+ const {plugin} = props;
+ const ref = useRef();
+ const toggleSections = (collapse: boolean) => {
+ const newSections = plugin.settings.spaces.map((s) => {
+ return {...s, collapsed: collapse}
+ });
+ plugin.settings.spaces = newSections;
+ plugin.saveSettings();
+ }
+
+ const newSection = () => {
+
+ let vaultChangeModal = new SectionChangeModal(plugin, '', 0, 'create');
+ vaultChangeModal.open();
+ }
+
+
+ useEffect(() => {
+ refreshLeafs();
+ }, [])
+
+ const refreshLeafs = () => {
+ // plugin.app.workspace.getLeavesOfType(FILE_TREE_VIEW_TYPE)
+ let ribbons = [];
+ let leafs = [];
+ let spaceActive = true;
+ if (plugin.app.workspace.leftSplit && platformIsMobile()) {
+ const mobileDrawer = (plugin.app.workspace.leftSplit as WorkspaceMobileDrawer);
+ const leaves = mobileDrawer.children as WorkspaceLeaf[]
+ const index = leaves.reduce((p: number,c,i) => {
+ return c.getViewState().type == FILE_TREE_VIEW_TYPE ? i : p
+ }, -1)
+ spaceActive = index == mobileDrawer.currentTab
+ leafs.push(...leaves.filter((l, i) => i != index))
+
+ }
+ if (plugin.app.workspace.leftRibbon && !props.plugin.settings.sidebarRibbon) {
+ ribbons.push(...plugin.app.workspace.leftRibbon.orderedRibbonActions)
+ }
+ return {ribbons, leafs, spaceActive}
+ }
+
+ const showMenu = (e: React.MouseEvent) => {
+ const {ribbons, spaceActive, leafs} = refreshLeafs();
+ const menu = new Menu();
+ !spaceActive && menu.addItem((menuItem) => {
+ menuItem.setIcon('lucide-arrow-left');
+ menuItem.setTitle(t.menu.backToSpace);
+ menuItem.onClick((ev: MouseEvent) => {
+ const leaves = plugin.app.workspace.getLeavesOfType(FILE_TREE_VIEW_TYPE)
+ if(leaves.length > 0){
+ plugin.app.workspace.revealLeaf(leaves[0])
+ }
+
+ });
+ })
+ menu.addItem((menuItem) => {
+ menuItem.setIcon('plus');
+ menuItem.setTitle(t.menu.newSpace);
+ menuItem.onClick((ev: MouseEvent) => {
+ newSection()
+ });
+ })
+
+ menu.addItem((menuItem) => {
+ menuItem.setIcon('lucide-chevrons-down-up');
+ menuItem.setTitle(t.menu.collapseAllSections);
+ menuItem.onClick((ev: MouseEvent) => {
+ toggleSections(true)
+ });
+ })
+
+ menu.addItem((menuItem) => {
+ menuItem.setIcon('lucide-chevrons-up-down');
+ menuItem.setTitle(t.menu.expandAllSections);
+ menuItem.onClick((ev: MouseEvent) => {
+ toggleSections(false)
+ });
+ })
+ menu.addSeparator();
+
+ leafs.map(l =>
+ menu.addItem((menuItem) => {
+ menuItem.setIcon(l.view.icon);
+ menuItem.setTitle(l.getDisplayText());
+ menuItem.onClick((ev: MouseEvent) => {
+ plugin.app.workspace.revealLeaf(l)
+ });
+ })
+ )
+
+ menu.addItem((menuItem) => {
+ menuItem.setIcon('lucide-settings');
+ menuItem.setTitle(t.menu.obSettings);
+ menuItem.onClick((ev: MouseEvent) => {
+ plugin.app.commands.commands['app:open-settings'].callback()
+ });
+ })
+
+ menu.addItem((menuItem) => {
+ menuItem.setIcon('vault');
+ menuItem.setTitle(t.menu.openVault);
+ menuItem.onClick((ev: MouseEvent) => {
+ plugin.app.commands.commands['app:open-vault'].callback()
+ });
+ })
+
+
+
+ menu.addSeparator();
+ ribbons.map(r =>
+ menu.addItem((menuItem) => {
+ menuItem.setIcon(r.icon);
+ menuItem.setTitle(r.title);
+ menuItem.onClick((ev: MouseEvent) => {
+ r.callback();
+ });
+ })
+ )
+ menu.addSeparator();
+ menu.addItem((menuItem) => {
+ menuItem.setIcon('mk-logo');
+ menuItem.setTitle(t.menu.getHelp);
+ menuItem.onClick((ev: MouseEvent) => {
+ window.open('https://make.md/community')
+ });
+ })
+ // if (isMouseEvent(e)) {
+ const offset = ref.current.getBoundingClientRect();
+ menu.showAtPosition({ x: offset.left, y: offset.top+30 });
+
+ }
+
+ return
+
showMenu(e)}>
+ {plugin.app.vault.getName()}
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/components/Spaces/NewNote.tsx b/src/components/Spaces/NewNote.tsx
new file mode 100644
index 0000000..e577361
--- /dev/null
+++ b/src/components/Spaces/NewNote.tsx
@@ -0,0 +1,31 @@
+import MakeMDPlugin from "main";
+import { useRecoilState } from "recoil";
+import { createNewMarkdownFile } from "utils/utils"
+import * as recoilState from 'recoil/pluginState';
+import React from 'react';
+import 'css/NewNote.css'
+import t from "i18n"
+import { uiIconSet } from "utils/icons";
+interface NewNotesComponentProps {
+ plugin: MakeMDPlugin;
+}
+
+export const NewNotes = (props: NewNotesComponentProps) => {
+ const [focusedFolder, setFocusedFolder] = useRecoilState(recoilState.focusedFolder);
+ const {plugin} = props
+ const newFile = async () => {
+ await createNewMarkdownFile(
+ props.plugin.app,
+ focusedFolder,
+ '',
+ ''
+ )
+ }
+ return
+
+
+
+}
\ No newline at end of file
diff --git a/src/components/Spaces/TreeView/FolderTreeView.tsx b/src/components/Spaces/TreeView/FolderTreeView.tsx
new file mode 100644
index 0000000..0d801b2
--- /dev/null
+++ b/src/components/Spaces/TreeView/FolderTreeView.tsx
@@ -0,0 +1,392 @@
+import MakeMDPlugin from 'main';
+import { addIcon, ButtonComponent, Component, Menu, SearchComponent, TAbstractFile, TFile, TFolder } from 'obsidian';
+import React, { useState, useMemo, useEffect, forwardRef, HTMLAttributes, useRef, CSSProperties } from 'react';
+import ReactDOM from 'react-dom'
+import { useRecoilState } from 'recoil';
+import { FlattenedTreeNode, FolderTree, SectionTree } from 'types/types';
+import * as Util from 'utils/utils';
+import * as recoilState from 'recoil/pluginState';
+import { AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
+import { UniqueIdentifier } from '@dnd-kit/core';
+import 'css/FolderTreeView.css';
+import classNames from 'classnames';
+import { MoveSuggestionModal, SectionChangeModal, VaultChangeModal } from 'components/Spaces/modals';
+import { isMouseEvent } from 'hooks/useLongPress'
+import { SectionItem } from 'components/Spaces/TreeView/SectionView';
+import { StickerModal } from '../FileStickerMenu/FileStickerMenu';
+import t from "i18n"
+import { usePopper } from 'react-popper';
+import { uiIconSet } from 'utils/icons';
+
+
+export enum IndicatorState {
+ None,
+ Top,
+ Bottom,
+ Row,
+}
+
+export type Indicator = {
+ state: IndicatorState;
+ depth: number
+} | undefined;
+
+export interface SortableTreeItemProps extends TreeItemProps {
+ id: UniqueIdentifier;
+ disabled: boolean;
+ }
+
+ const animateLayoutChanges: AnimateLayoutChanges = ({isSorting, wasDragging}) =>
+ isSorting || wasDragging ? false : true;
+
+ export const SortableTreeItem = ({id, data, depth, disabled, style, ...props}: SortableTreeItemProps) => {
+ const {
+ attributes,
+ isDragging,
+ isSorting,
+ listeners,
+ setDraggableNodeRef,
+ setDroppableNodeRef,
+ transform,
+ transition,
+ } = useSortable({
+ id,
+ animateLayoutChanges,
+ disabled,
+ data
+ });
+
+ const memoListeners = useMemo(() => {
+ return {
+ ...attributes,
+ ...listeners
+ }
+ }, [isSorting])
+
+
+
+ if (data.parentId == null) {
+ return
+ } else
+ {
+ return (
+
+ );
+ }
+ }
+
+export interface TreeItemProps {
+ childCount?: number;
+ clone?: boolean;
+ collapsed?: boolean;
+ depth: number;
+ disableInteraction?: boolean;
+ disableSelection?: boolean;
+ disabled: boolean;
+ ghost?: boolean;
+ handleProps?: any;
+ indicator: Indicator;
+ indentationWidth: number;
+ data: FlattenedTreeNode;
+ plugin: MakeMDPlugin;
+ style: CSSProperties;
+ onCollapse?(folder: TFolder): void;
+ wrapperRef?(node: HTMLDivElement): void;
+ }
+
+
+export const TreeItem = forwardRef(
+ (
+ {
+ childCount,
+ clone,
+ data,
+ depth,
+ disableSelection,
+ disableInteraction,
+ ghost,
+ handleProps,
+ indentationWidth,
+ indicator,
+ collapsed,
+ onCollapse,
+ wrapperRef,
+ style,
+ plugin,
+ disabled,
+ },
+ ref
+ ) => {
+
+ const [activeFile, setActiveFile] = useRecoilState(recoilState.activeFile);
+ const [sections, setSections] = useRecoilState(recoilState.sections);
+ const [fileIcons, setFileIcons] = useRecoilState(recoilState.fileIcons);
+ const [referenceElement, setReferenceElement] = React.useState(null);
+ const [popperElement, setPopperElement] = useState(null)
+ const { styles, attributes } = usePopper(referenceElement, popperElement)
+
+ const openFile = (file: FlattenedTreeNode, e: React.MouseEvent) => {
+ Util.openFile(file, plugin.app, e.ctrlKey || e.metaKey);
+ setActiveFile(file.path);
+ };
+
+
+ const updateSections = (sections: SectionTree[]) => {
+ plugin.settings.spaces = sections;
+ plugin.saveSettings();
+ }
+
+ const triggerStickerMenu = (e: React.MouseEvent | React.TouchEvent) => {
+ let vaultChangeModal = new StickerModal(plugin.app, (emoji) => saveFileIcon(emoji));
+ vaultChangeModal.open();
+ }
+ const triggerContextMenu = (file: TAbstractFile, isFolder: boolean, e: React.MouseEvent | React.TouchEvent) => {
+ const fileMenu = new Menu();
+ if (isFolder) {
+ fileMenu.addSeparator();
+ fileMenu.addItem((menuItem) => {
+ menuItem.setIcon('edit');
+ menuItem.setTitle(t.buttons.createNote);
+ menuItem.onClick((ev: MouseEvent) => {
+ newFileInFolder();
+ });
+ })
+ fileMenu.addItem((menuItem) => {
+ menuItem.setIcon('folder-plus');
+ menuItem.setTitle(t.buttons.createFolder);
+ menuItem.onClick((ev: MouseEvent) => {
+ let vaultChangeModal = new VaultChangeModal(plugin, data, 'create folder', -1);
+ vaultChangeModal.open();
+ });
+ })
+ }
+
+ // Pin - Unpin Item
+ fileMenu.addSeparator();
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.spaceTitle);
+ menuItem.setDisabled(true);
+
+ })
+ sections.map((f, i) => {
+ fileMenu.addItem((menuItem) => {
+ menuItem.setIcon('pin');
+ if (f.children.contains(file.path)) {
+ menuItem.setIcon('checkmark');menuItem.setTitle(f.section);
+ }
+ else { menuItem.setTitle(f.section);
+ menuItem.setIcon('plus');
+ }
+ menuItem.onClick((ev: MouseEvent) => {
+ updateSections(!sections[i].children.contains(file.path) ? sections.map((s,k) => {
+ return k == i ?
+ {
+ ...s,
+ children: [file.path, ...s.children]
+ } : s
+ }) : sections.map((s,k) => {
+ return k == i ?
+ {
+ ...s,
+ children: s.children.filter(g => g != file.path)
+ } : s
+ }))
+ // const newPinnedFiles = (pinnedFiles.contains(file)) ?pinnedFiles.filter((pinnedFile) => pinnedFile !== file) : [...pinnedFiles, file];
+ // setPinnedFiles(newPinnedFiles);
+ // plugin.settings.sections = newPinnedFiles.map(f => f.path);
+ // plugin.saveSettings();
+ });
+ });
+ })
+
+ fileMenu.addSeparator();
+ // Rename Item
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.rename);
+ menuItem.setIcon('pencil');
+ menuItem.onClick((ev: MouseEvent) => {
+ let vaultChangeModal = new VaultChangeModal(plugin, file, 'rename');
+ vaultChangeModal.open();
+ });
+ });
+
+ // Delete Item
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle('Delete');
+ menuItem.setIcon('trash');
+ menuItem.onClick((ev: MouseEvent) => {
+ let deleteOption = plugin.settings.deleteFileOption;
+ if (deleteOption === 'permanent') {
+ plugin.app.vault.delete(file, true);
+ } else if (deleteOption === 'system-trash') {
+ plugin.app.vault.trash(file, true);
+ } else if (deleteOption === 'trash') {
+ plugin.app.vault.trash(file, false);
+ }
+ });
+ });
+
+ // Open in a New Pane
+ fileMenu.addItem((menuItem) => {
+ menuItem.setIcon('go-to-file');
+ menuItem.setTitle(t.menu.openFilePane);
+ menuItem.onClick((ev: MouseEvent) => {
+ // @ts-ignore
+ Util.openFileInNewPane(plugin, {...file, isFolder: isFolder});
+ });
+ });
+
+ // Make a Copy Item
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.duplicate);
+ menuItem.setIcon('documents');
+ menuItem.onClick((ev: MouseEvent) => {
+ if ((file as TFile).basename && (file as TFile).extension)
+ plugin.app.vault.copy(file as TFile, `${file.parent.path}/${(file as TFile).basename} 1.${(file as TFile).extension}`);
+ });
+ });
+
+ // Move Item
+ if (!Util.internalPluginLoaded('file-explorer', plugin.app)) {
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.moveFile);
+ menuItem.setIcon('paper-plane');
+ menuItem.onClick((ev: MouseEvent) => {
+ let fileMoveSuggester = new MoveSuggestionModal(plugin.app, file as TFile);
+ fileMoveSuggester.open();
+ });
+ });
+ }
+ // Trigger
+ plugin.app.workspace.trigger('file-menu', fileMenu, file, 'file-explorer');
+ if (isMouseEvent(e)) {
+ fileMenu.showAtPosition({ x: e.pageX, y: e.pageY });
+ } else {
+ // @ts-ignore
+ fileMenu.showAtPosition({ x: e.nativeEvent.locationX, y: e.nativeEvent.locationY });
+ }
+ return false;
+ };
+
+ const newFileInFolder = async () => {
+ await Util.createNewMarkdownFile(
+ plugin.app,
+ data.parent.children.find(f => f.name == data.name) as TFolder,
+ '',
+ ''
+ )
+ }
+
+ const fileIcon = fileIcons.find(([path, icon]) => path == data.path);
+ const saveFileIcon = (icon: string) =>
+ {
+ const newFileIcons = [...fileIcons.filter(f => f[0] != data.path), [data.path, icon]] as [string, string][]
+ plugin.settings.fileIcons = newFileIcons;
+ plugin.saveSettings();
+ }
+
+ return (<>
+
+
+
openFile(data, e)}
+
+ onContextMenu={(e) => triggerContextMenu(plugin.app.vault.getAbstractFileByPath(data.path), data.isFolder, e)}
+
+ >
+
+ { data.isFolder &&
+
+ }
+ { plugin.settings.spacesStickers &&
+
+
+
}
+
{
+ data.isFolder ? data.name : data.name.substring(0, data.name.lastIndexOf('.')) || data.name
+ }
+
+ { !clone ?
+
+
+ {data.isFolder &&
+
+ }
+
: <>>
+ }
+
+
+ {/* {data.isFolder && !collapsed && data.children.length == 0 &&
+ No Notes Inside
} */}
+ >
+ );
+ }
+ );
+
+
+ TreeItem.displayName = 'TreeItem';
diff --git a/src/components/Spaces/TreeView/SectionView.tsx b/src/components/Spaces/TreeView/SectionView.tsx
new file mode 100644
index 0000000..e6ea2ee
--- /dev/null
+++ b/src/components/Spaces/TreeView/SectionView.tsx
@@ -0,0 +1,208 @@
+import { Menu } from 'obsidian';
+import React, { forwardRef } from 'react';
+import { useRecoilState } from 'recoil';
+import { SectionTree } from 'types/types';
+import * as Util from 'utils/utils';
+import * as recoilState from 'recoil/pluginState';
+import 'css/SectionView.css';
+import classNames from 'classnames';
+import { SectionChangeModal, VaultChangeModal } from 'components/Spaces/modals';
+
+import path from 'path';
+import { IndicatorState, TreeItemProps } from 'components/Spaces/TreeView/FolderTreeView';
+import t from "i18n"
+import { isMouseEvent } from 'hooks/useLongPress';
+import { uiIconSet } from 'utils/icons';
+
+
+export const SectionItem = forwardRef(
+ (
+ {
+ childCount,
+ clone,
+ data,
+ depth,
+ disableSelection,
+ disableInteraction,
+ ghost,
+ handleProps,
+ indentationWidth,
+ indicator,
+ collapsed,
+ style,
+ onCollapse,
+ wrapperRef,
+ plugin,
+ disabled,
+ },
+ ref
+ ) => {
+ const [sections, setSections] = useRecoilState(recoilState.sections);
+ const [focusedFolder, setFocusedFolder] = useRecoilState(recoilState.focusedFolder)
+ const section = sections.find((s, i) => {
+ return i == data.section
+ })
+ const newFolderInSection = () => {
+ let vaultChangeModal = new VaultChangeModal(plugin, focusedFolder, 'create folder', data.section);
+ vaultChangeModal.open();
+ }
+ const newFileInSection = async () => {
+
+ const newFile = await Util.createNewMarkdownFile(
+ plugin.app,
+ focusedFolder,
+ '')
+ if (data.section != -1)
+ updateSections(sections.map((f, i) => {
+ return i == data.section ? {
+ ...f,
+ children: [newFile.path, ...f.children]
+ } : f
+ }))
+ }
+ const updateSections = (sections: SectionTree[]) => {
+ plugin.settings.spaces = sections;
+ plugin.saveSettings();
+ }
+
+
+const triggerMenu = (e: React.MouseEvent | React.TouchEvent) => {
+ data.section == -1 ? triggerVaultMenu(e) : triggerSectionMenu(data.name, data.index, e)
+}
+ const triggerSectionMenu = (section: string, index: number, e: React.MouseEvent | React.TouchEvent) => {
+ const fileMenu = new Menu();
+
+// fileMenu.addItem((menuItem) => {
+// menuItem.setTitle(t.menu.collapseAllFolders);
+// menuItem.setIcon('lucide-chevrons-down-up');
+// menuItem.onClick((ev: MouseEvent) => {
+
+// });
+// });
+
+
+// fileMenu.addItem((menuItem) => {
+// menuItem.setTitle(t.menu.expandAllFolders);
+// menuItem.setIcon('lucide-chevrons-down-dow ');
+// menuItem.onClick((ev: MouseEvent) => {
+
+// });
+// });
+
+ // Rename Item
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.edit);
+ menuItem.setIcon('pencil');
+ menuItem.onClick((ev: MouseEvent) => {
+ let vaultChangeModal = new SectionChangeModal(plugin, section, index, 'rename');
+ vaultChangeModal.open();
+ });
+ });
+
+ // Delete Item
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.delete);
+ menuItem.setIcon('trash');
+ menuItem.onClick((ev: MouseEvent) => {
+ updateSections(sections.filter((s, i) => {
+ return i != index
+ }))
+ });
+ });
+
+
+
+ if (isMouseEvent(e)) {
+ fileMenu.showAtPosition({ x: e.pageX, y: e.pageY });
+ } else {
+ // @ts-ignore
+ fileMenu.showAtPosition({ x: e.nativeEvent.locationX, y: e.nativeEvent.locationY });
+ }
+ return false;
+};
+
+const triggerVaultMenu = (e: React.MouseEvent | React.TouchEvent) => {
+ const fileMenu = new Menu();
+
+ // Rename Item
+ fileMenu.addItem((menuItem) => {
+ menuItem.setTitle(t.menu.newSpace);
+ menuItem.setIcon('plus');
+ menuItem.onClick((ev: MouseEvent) => {
+ let vaultChangeModal = new SectionChangeModal(plugin, '', 0, 'create');
+ vaultChangeModal.open();
+ });
+ if (isMouseEvent(e)) {
+ fileMenu.showAtPosition({ x: e.pageX, y: e.pageY });
+ } else {
+ // @ts-ignore
+ fileMenu.showAtPosition({ x: e.nativeEvent.locationX, y: e.nativeEvent.locationY });
+ }
+ return false;
+ });
+}
+ return (<>
+
+
+
+
triggerMenu(e)}
+ onClick={(e) => onCollapse(data)}
+ ref={ref}
+ { ...handleProps}>
+
{data.id == '/' ? plugin.app.vault.getName() : data.name}
+
+
+
+
+
+
+
+
+
+
+
+ {section && !collapsed && section.children.length == 0 &&
+ No Notes Inside
}
+ >
+ );
+ }
+ );
+
+
+ SectionItem.displayName = 'SectionItem';
diff --git a/src/components/Spaces/modals.ts b/src/components/Spaces/modals.ts
new file mode 100644
index 0000000..bcd3d7d
--- /dev/null
+++ b/src/components/Spaces/modals.ts
@@ -0,0 +1,248 @@
+import { Modal, App, TFolder, TFile, TAbstractFile, FuzzySuggestModal } from 'obsidian';
+import MakeMDPlugin from 'main';
+import { createNewMarkdownFile } from 'utils/utils';
+import { SectionTree, eventTypes } from 'types/types';
+import t from 'i18n'
+type Action = 'rename' | 'create folder' | 'create note';
+type SectionAction = 'rename' | 'create';
+
+export class VaultChangeModal extends Modal {
+ file: TFolder | TFile | TAbstractFile;
+ action: Action;
+ plugin: MakeMDPlugin;
+ section: number;
+
+ constructor(plugin: MakeMDPlugin, file: TFolder | TFile | TAbstractFile, action: Action, section?: number) {
+ super(plugin.app);
+ this.file = file;
+ this.action = action;
+ this.plugin = plugin;
+ this.section = section;
+ }
+
+ onOpen() {
+ let { contentEl } = this;
+ let myModal = this;
+
+ // Header
+ let headerText: string;
+
+ if (this.action === 'rename') {
+ headerText = t.labels.rename;
+ } else if (this.action === 'create folder') {
+ headerText = t.labels.createFolder;
+ } else if (this.action === 'create note') {
+ headerText = t.labels.createNote;
+ }
+
+ const headerEl = contentEl.createEl('div', { text: headerText });
+ headerEl.addClass('modal-title');
+
+ // Input El
+ const inputEl = contentEl.createEl('input');
+
+ inputEl.style.cssText = 'width: 100%; height: 2.5em; margin-bottom: 15px;';
+ if (this.action === 'rename') {
+ // Manual Rename Handler For md Files
+ if (this.file.name.endsWith('.md')) {
+ inputEl.value = this.file.name.substring(0, this.file.name.lastIndexOf('.'));
+ } else {
+ inputEl.value = this.file.name;
+ }
+ }
+
+ inputEl.focus();
+
+ // Buttons
+ let changeButtonText: string;
+
+ if (this.action === 'rename') {
+ changeButtonText = t.buttons.rename;
+ } else if (this.action === 'create folder') {
+ changeButtonText = t.buttons.createFolder;
+ } else if (this.action === 'create note') {
+ changeButtonText = t.buttons.createNote;
+ }
+
+ const changeButton = contentEl.createEl('button', { text: changeButtonText });
+
+ const cancelButton = contentEl.createEl('button', { text: t.buttons.cancel });
+ cancelButton.style.cssText = 'float: right;';
+ cancelButton.addEventListener('click', () => {
+ myModal.close();
+ });
+
+ const updateSections = (sections: SectionTree[]) => {
+ this.plugin.settings.spaces = sections;
+ this.plugin.saveSettings();
+
+ }
+
+ const onClickAction = async () => {
+ let newName = inputEl.value;
+ if (this.action === 'rename') {
+ // Manual Rename Handler For md Files
+ if (this.file.name.endsWith('.md')) newName = newName + '.md';
+ this.app.fileManager.renameFile(this.file, this.file.parent.path + '/' + newName);
+ } else if (this.action === 'create folder') {
+ const path = this.file.path + '/' + newName;
+ this.app.vault.createFolder(path);
+ if (this.section >= 0)
+ updateSections(this.plugin.settings.spaces.map((s,k) => {
+ return k == this.section ?
+ {
+ ...s,
+ children: [newName, ...s.children]
+ } : s
+ }))
+ } else if (this.action === 'create note') {
+ await createNewMarkdownFile(
+ this.plugin.app,
+ this.file as TFolder,
+ newName,
+ ''
+ );
+ }
+ myModal.close();
+ };
+
+ // Event Listener
+ changeButton.addEventListener('click', onClickAction);
+ inputEl.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') onClickAction();
+ });
+ }
+
+ onClose() {
+ let { contentEl } = this;
+ contentEl.empty();
+ }
+}
+
+export class MoveSuggestionModal extends FuzzySuggestModal {
+ app: App;
+ fileOrFolderToMove: TFile | TFolder;
+
+ constructor(app: App, fileOrFolderToMove: TFile | TFolder) {
+ super(app);
+ this.fileOrFolderToMove = fileOrFolderToMove;
+ }
+
+ getItemText(item: TFolder): string {
+ return item.path;
+ }
+
+ getItems(): TFolder[] {
+ return getAllFoldersInVault(this.app);
+ }
+
+ onChooseItem(item: TFolder, evt: MouseEvent | KeyboardEvent) {
+ this.app.vault.rename(this.fileOrFolderToMove, item.path + '/' + this.fileOrFolderToMove.name);
+ }
+}
+
+function getAllFoldersInVault(app: App): TFolder[] {
+ let folders: TFolder[] = [];
+ let rootFolder = app.vault.getRoot();
+ folders.push(rootFolder);
+ function recursiveFx(folder: TFolder) {
+ for (let child of folder.children) {
+ if (child instanceof TFolder) {
+ let childFolder: TFolder = child as TFolder;
+ folders.push(childFolder);
+ if (childFolder.children) recursiveFx(childFolder);
+ }
+ }
+ }
+ recursiveFx(rootFolder);
+ return folders;
+}
+
+export class SectionChangeModal extends Modal {
+ section: string;
+ sectionIndex: number;
+ action: SectionAction;
+ plugin: MakeMDPlugin;
+
+ constructor(plugin: MakeMDPlugin, section: string, sectionIndex: number, action: SectionAction) {
+ super(plugin.app);
+ this.section = section;
+ this.sectionIndex = sectionIndex;
+ this.action = action;
+ this.plugin = plugin;
+ }
+
+ onOpen() {
+ let { contentEl } = this;
+ let myModal = this;
+
+ // Header
+ let headerText: string;
+
+ if (this.action === 'rename') {
+ headerText = t.labels.renameSection;
+ } else if (this.action === 'create') {
+ headerText = t.labels.createSection;
+ }
+
+ const headerEl = contentEl.createEl('div', { text: headerText });
+ headerEl.addClass('modal-title');
+
+ // Input El
+ const inputEl = contentEl.createEl('input');
+
+ inputEl.style.cssText = 'width: 100%; height: 2.5em; margin-bottom: 15px;';
+ if (this.action === 'rename') {
+ inputEl.value = this.section;
+ }
+
+ inputEl.focus();
+
+ // Buttons
+ let changeButtonText: string;
+
+ if (this.action === 'rename') {
+ changeButtonText = t.buttons.rename;
+ } else if (this.action === 'create') {
+ changeButtonText = t.buttons.createSection;
+ }
+
+ const changeButton = contentEl.createEl('button', { text: changeButtonText });
+
+ const cancelButton = contentEl.createEl('button', { text: t.buttons.cancel });
+ cancelButton.style.cssText = 'float: right;';
+ cancelButton.addEventListener('click', () => {
+ myModal.close();
+ });
+
+ const updateSections = (sections: SectionTree[]) => {
+ this.plugin.settings.spaces = sections;
+ this.plugin.saveSettings();
+
+ }
+ const onClickAction = async () => {
+ let newName = inputEl.value;
+ if (this.action === 'rename') {
+ updateSections(this.plugin.settings.spaces.map((s, i) => {
+ return i == this.sectionIndex ? {
+ ...s, section: newName
+ } : s
+ }));
+ } else if (this.action === 'create') {
+ updateSections([{section: newName, children: [], collapsed: false}, ...this.plugin.settings.spaces]);
+ }
+ myModal.close();
+ };
+
+ // Event Listener
+ changeButton.addEventListener('click', onClickAction);
+ inputEl.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') onClickAction();
+ });
+ }
+
+ onClose() {
+ let { contentEl } = this;
+ contentEl.empty();
+ }
+}
\ No newline at end of file
diff --git a/src/components/StickerMenu/StickerMenu.tsx b/src/components/StickerMenu/StickerMenu.tsx
new file mode 100644
index 0000000..41c908e
--- /dev/null
+++ b/src/components/StickerMenu/StickerMenu.tsx
@@ -0,0 +1,112 @@
+
+import MakeMDPlugin from "main"
+import { renderToStaticMarkup } from "react-dom/server"
+import React from 'react'
+import 'css/StickerMenu.css'
+import ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client'
+import {
+ App,
+ Editor,
+ EditorPosition,
+ EditorSuggest,
+ EditorSuggestContext,
+ EditorSuggestTriggerInfo,
+ TFile,
+} from "obsidian"
+import { Emoji, EmojiData } from "./emojis"
+import t from "i18n"
+import { emojis } from "./emojis/default"
+import { unifiedToNative } from "utils/utils";
+
+export default class StickerMenu extends EditorSuggest {
+ inCmd = false
+ cmdStartCh = 0
+ plugin: MakeMDPlugin
+
+ constructor(app: App, plugin: MakeMDPlugin) {
+ super(app)
+ this.plugin = plugin
+ this.emojis = Object.keys(emojis as EmojiData).reduce((p,c) => [...p, ...emojis[c].map(e => ({ label: e.n[0], desc: e.n[1], variants: e.v, unicode: e.u}))], [])
+
+ }
+ resetInfos() {
+ this.cmdStartCh = 0
+ this.inCmd = false
+ }
+
+ onTrigger(
+ cursor: EditorPosition,
+ editor: Editor,
+ _file: TFile
+ ): EditorSuggestTriggerInfo {
+ const currentLine = editor.getLine(cursor.line).slice(0, cursor.ch)
+
+ if (
+ !this.inCmd &&
+ !(currentLine.slice(-2) ==
+ " "+this.plugin.settings.emojiTriggerChar || currentLine[0] ==
+ this.plugin.settings.emojiTriggerChar)
+ ) {
+ this.resetInfos()
+ return null
+ }
+
+ if (!this.inCmd) {
+ this.cmdStartCh = currentLine.length - 1
+ this.inCmd = true
+ }
+
+ const currentCmd = currentLine.slice(this.cmdStartCh, cursor.ch)
+
+ if (
+ currentCmd.includes(" ") ||
+ !currentCmd.includes(this.plugin.settings.emojiTriggerChar)
+ ) {
+ this.resetInfos()
+ return null
+ }
+ // @ts-ignore
+ this.suggestEl.classList.toggle('mk-emoji-menu', true);
+ return { start: cursor, end: cursor, query: currentCmd.slice(1) }
+ }
+
+ emojis: Emoji[]
+
+
+
+ getSuggestions(
+ context: EditorSuggestContext
+ ): Emoji[] | Promise {
+ const suggestions = this.emojis.filter(({ label, desc }) =>
+ label.includes(context.query) || desc?.includes(context.query)
+ )
+
+ return suggestions.length > 0
+ ? suggestions
+ : [{ label: t.commandsSuggest.noResult, unicode: '', desc: '' }]
+ }
+
+
+ renderSuggestion(value: Emoji, el: HTMLElement): void {
+ const div = document.createElement("div")
+ div.setAttribute('aria-label', value.label)
+ const reactElement = createRoot(div)
+ reactElement.render(<>{value.unicode.length > 0 ? unifiedToNative(value.unicode): t.commandsSuggest.noResult}>)
+ el.appendChild(div)
+ }
+
+ selectSuggestion(cmd: Emoji, _evt: MouseEvent | KeyboardEvent): void {
+ if (cmd.label === t.commandsSuggest.noResult) return
+
+ this.context.editor.replaceRange(
+ unifiedToNative(cmd.unicode),
+ { ...this.context.start, ch: this.cmdStartCh },
+ this.context.end
+ )
+
+ this.resetInfos()
+
+ this.close()
+ }
+}
diff --git a/src/components/StickerMenu/emojis/default.ts b/src/components/StickerMenu/emojis/default.ts
new file mode 100644
index 0000000..fe69dc4
--- /dev/null
+++ b/src/components/StickerMenu/emojis/default.ts
@@ -0,0 +1,3 @@
+import { EmojiData } from ".";
+
+export const emojis = {"smileys_people":[{"n":["grinning","grinning face"],"u":"1f600"},{"n":["smiley","smiling face with open mouth"],"u":"1f603"},{"n":["smile","smiling face with open mouth and smiling eyes"],"u":"1f604"},{"n":["grin","grinning face with smiling eyes"],"u":"1f601"},{"n":["laughing","satisfied","smiling face with open mouth and tightly-closed eyes"],"u":"1f606"},{"n":["sweat smile","smiling face with open mouth and cold sweat"],"u":"1f605"},{"n":["rolling on the floor laughing"],"u":"1f923"},{"n":["joy","face with tears of joy"],"u":"1f602"},{"n":["slightly smiling face"],"u":"1f642"},{"n":["upside-down face","upside down face"],"u":"1f643"},{"n":["melting face"],"u":"1fae0"},{"n":["wink","winking face"],"u":"1f609"},{"n":["blush","smiling face with smiling eyes"],"u":"1f60a"},{"n":["innocent","smiling face with halo"],"u":"1f607"},{"n":["smiling face with 3 hearts","smiling face with smiling eyes and three hearts"],"u":"1f970"},{"n":["heart eyes","smiling face with heart-shaped eyes"],"u":"1f60d"},{"n":["star-struck","grinning face with star eyes"],"u":"1f929"},{"n":["kissing heart","face throwing a kiss"],"u":"1f618"},{"n":["kissing","kissing face"],"u":"1f617"},{"n":["relaxed","white smiling face"],"u":"263a-fe0f"},{"n":["kissing closed eyes","kissing face with closed eyes"],"u":"1f61a"},{"n":["kissing smiling eyes","kissing face with smiling eyes"],"u":"1f619"},{"n":["smiling face with tear"],"u":"1f972"},{"n":["yum","face savouring delicious food"],"u":"1f60b"},{"n":["stuck out tongue","face with stuck-out tongue"],"u":"1f61b"},{"n":["stuck out tongue winking eye","face with stuck-out tongue and winking eye"],"u":"1f61c"},{"n":["zany face","grinning face with one large and one small eye"],"u":"1f92a"},{"n":["stuck out tongue closed eyes","face with stuck-out tongue and tightly-closed eyes"],"u":"1f61d"},{"n":["money-mouth face","money mouth face"],"u":"1f911"},{"n":["hugging face"],"u":"1f917"},{"n":["face with hand over mouth","smiling face with smiling eyes and hand covering mouth"],"u":"1f92d"},{"n":["face with open eyes and hand over mouth"],"u":"1fae2"},{"n":["face with peeking eye"],"u":"1fae3"},{"n":["shushing face","face with finger covering closed lips"],"u":"1f92b"},{"n":["thinking face"],"u":"1f914"},{"n":["saluting face"],"u":"1fae1"},{"n":["zipper-mouth face","zipper mouth face"],"u":"1f910"},{"n":["face with raised eyebrow","face with one eyebrow raised"],"u":"1f928"},{"n":["neutral face"],"u":"1f610"},{"n":["expressionless","expressionless face"],"u":"1f611"},{"n":["no mouth","face without mouth"],"u":"1f636"},{"n":["dotted line face"],"u":"1fae5"},{"n":["face in clouds"],"u":"1f636-200d-1f32b-fe0f"},{"n":["smirk","smirking face"],"u":"1f60f"},{"n":["unamused","unamused face"],"u":"1f612"},{"n":["face with rolling eyes"],"u":"1f644"},{"n":["grimacing","grimacing face"],"u":"1f62c"},{"n":["face exhaling"],"u":"1f62e-200d-1f4a8"},{"n":["lying face"],"u":"1f925"},{"n":["relieved","relieved face"],"u":"1f60c"},{"n":["pensive","pensive face"],"u":"1f614"},{"n":["sleepy","sleepy face"],"u":"1f62a"},{"n":["drooling face"],"u":"1f924"},{"n":["sleeping","sleeping face"],"u":"1f634"},{"n":["mask","face with medical mask"],"u":"1f637"},{"n":["face with thermometer"],"u":"1f912"},{"n":["face with head-bandage","face with head bandage"],"u":"1f915"},{"n":["nauseated face"],"u":"1f922"},{"n":["face vomiting","face with open mouth vomiting"],"u":"1f92e"},{"n":["sneezing face"],"u":"1f927"},{"n":["hot face","overheated face"],"u":"1f975"},{"n":["cold face","freezing face"],"u":"1f976"},{"n":["woozy face","face with uneven eyes and wavy mouth"],"u":"1f974"},{"n":["dizzy face"],"u":"1f635"},{"n":["face with spiral eyes"],"u":"1f635-200d-1f4ab"},{"n":["exploding head","shocked face with exploding head"],"u":"1f92f"},{"n":["face with cowboy hat"],"u":"1f920"},{"n":["partying face","face with party horn and party hat"],"u":"1f973"},{"n":["disguised face"],"u":"1f978"},{"n":["sunglasses","smiling face with sunglasses"],"u":"1f60e"},{"n":["nerd face"],"u":"1f913"},{"n":["face with monocle"],"u":"1f9d0"},{"n":["confused","confused face"],"u":"1f615"},{"n":["face with diagonal mouth"],"u":"1fae4"},{"n":["worried","worried face"],"u":"1f61f"},{"n":["slightly frowning face"],"u":"1f641"},{"n":["frowning face","white frowning face"],"u":"2639-fe0f"},{"n":["open mouth","face with open mouth"],"u":"1f62e"},{"n":["hushed","hushed face"],"u":"1f62f"},{"n":["astonished","astonished face"],"u":"1f632"},{"n":["flushed","flushed face"],"u":"1f633"},{"n":["pleading face","face with pleading eyes"],"u":"1f97a"},{"n":["face holding back tears"],"u":"1f979"},{"n":["frowning","frowning face with open mouth"],"u":"1f626"},{"n":["anguished","anguished face"],"u":"1f627"},{"n":["fearful","fearful face"],"u":"1f628"},{"n":["cold sweat","face with open mouth and cold sweat"],"u":"1f630"},{"n":["disappointed relieved","disappointed but relieved face"],"u":"1f625"},{"n":["cry","crying face"],"u":"1f622"},{"n":["sob","loudly crying face"],"u":"1f62d"},{"n":["scream","face screaming in fear"],"u":"1f631"},{"n":["confounded","confounded face"],"u":"1f616"},{"n":["persevere","persevering face"],"u":"1f623"},{"n":["disappointed","disappointed face"],"u":"1f61e"},{"n":["sweat","face with cold sweat"],"u":"1f613"},{"n":["weary","weary face"],"u":"1f629"},{"n":["tired face"],"u":"1f62b"},{"n":["yawning face"],"u":"1f971"},{"n":["triumph","face with look of triumph"],"u":"1f624"},{"n":["rage","pouting face"],"u":"1f621"},{"n":["angry","angry face"],"u":"1f620"},{"n":["face with symbols on mouth","serious face with symbols covering mouth"],"u":"1f92c"},{"n":["smiling imp","smiling face with horns"],"u":"1f608"},{"n":["imp"],"u":"1f47f"},{"n":["skull"],"u":"1f480"},{"n":["skull and crossbones"],"u":"2620-fe0f"},{"n":["poop","shit","hankey","pile of poo"],"u":"1f4a9"},{"n":["clown face"],"u":"1f921"},{"n":["japanese ogre"],"u":"1f479"},{"n":["japanese goblin"],"u":"1f47a"},{"n":["ghost"],"u":"1f47b"},{"n":["alien","extraterrestrial alien"],"u":"1f47d"},{"n":["alien monster","space invader"],"u":"1f47e"},{"n":["robot face"],"u":"1f916"},{"n":["smiley cat","smiling cat face with open mouth"],"u":"1f63a"},{"n":["smile cat","grinning cat face with smiling eyes"],"u":"1f638"},{"n":["joy cat","cat face with tears of joy"],"u":"1f639"},{"n":["heart eyes cat","smiling cat face with heart-shaped eyes"],"u":"1f63b"},{"n":["smirk cat","cat face with wry smile"],"u":"1f63c"},{"n":["kissing cat","kissing cat face with closed eyes"],"u":"1f63d"},{"n":["scream cat","weary cat face"],"u":"1f640"},{"n":["crying cat face"],"u":"1f63f"},{"n":["pouting cat","pouting cat face"],"u":"1f63e"},{"n":["see no evil","see-no-evil monkey"],"u":"1f648"},{"n":["hear no evil","hear-no-evil monkey"],"u":"1f649"},{"n":["speak no evil","speak-no-evil monkey"],"u":"1f64a"},{"n":["kiss","kiss mark"],"u":"1f48b"},{"n":["love letter"],"u":"1f48c"},{"n":["cupid","heart with arrow"],"u":"1f498"},{"n":["gift heart","heart with ribbon"],"u":"1f49d"},{"n":["sparkling heart"],"u":"1f496"},{"n":["heartpulse","growing heart"],"u":"1f497"},{"n":["heartbeat","beating heart"],"u":"1f493"},{"n":["revolving hearts"],"u":"1f49e"},{"n":["two hearts"],"u":"1f495"},{"n":["heart decoration"],"u":"1f49f"},{"n":["heart exclamation","heavy heart exclamation mark ornament"],"u":"2763-fe0f"},{"n":["broken heart"],"u":"1f494"},{"n":["heart on fire"],"u":"2764-fe0f-200d-1f525"},{"n":["mending heart"],"u":"2764-fe0f-200d-1fa79"},{"n":["heart","heavy black heart"],"u":"2764-fe0f"},{"n":["orange heart"],"u":"1f9e1"},{"n":["yellow heart"],"u":"1f49b"},{"n":["green heart"],"u":"1f49a"},{"n":["blue heart"],"u":"1f499"},{"n":["purple heart"],"u":"1f49c"},{"n":["brown heart"],"u":"1f90e"},{"n":["black heart"],"u":"1f5a4"},{"n":["white heart"],"u":"1f90d"},{"n":["100","hundred points symbol"],"u":"1f4af"},{"n":["anger","anger symbol"],"u":"1f4a2"},{"n":["boom","collision","collision symbol"],"u":"1f4a5"},{"n":["dizzy","dizzy symbol"],"u":"1f4ab"},{"n":["sweat drops","splashing sweat symbol"],"u":"1f4a6"},{"n":["dash","dash symbol"],"u":"1f4a8"},{"n":["hole"],"u":"1f573-fe0f"},{"n":["bomb"],"u":"1f4a3"},{"n":["speech balloon"],"u":"1f4ac"},{"n":["eye in speech bubble","eye-in-speech-bubble"],"u":"1f441-fe0f-200d-1f5e8-fe0f"},{"n":["left speech bubble"],"u":"1f5e8-fe0f"},{"n":["right anger bubble"],"u":"1f5ef-fe0f"},{"n":["thought balloon"],"u":"1f4ad"},{"n":["zzz","sleeping symbol"],"u":"1f4a4"},{"n":["wave","waving hand sign"],"u":"1f44b","v":["1f44b-1f3fb","1f44b-1f3fc","1f44b-1f3fd","1f44b-1f3fe","1f44b-1f3ff"]},{"n":["raised back of hand"],"u":"1f91a","v":["1f91a-1f3fb","1f91a-1f3fc","1f91a-1f3fd","1f91a-1f3fe","1f91a-1f3ff"]},{"n":["hand with fingers splayed","raised hand with fingers splayed"],"u":"1f590-fe0f","v":["1f590-1f3fb","1f590-1f3fc","1f590-1f3fd","1f590-1f3fe","1f590-1f3ff"]},{"n":["hand","raised hand"],"u":"270b","v":["270b-1f3fb","270b-1f3fc","270b-1f3fd","270b-1f3fe","270b-1f3ff"]},{"n":["spock-hand","raised hand with part between middle and ring fingers"],"u":"1f596","v":["1f596-1f3fb","1f596-1f3fc","1f596-1f3fd","1f596-1f3fe","1f596-1f3ff"]},{"n":["rightwards hand"],"u":"1faf1","v":["1faf1-1f3fb","1faf1-1f3fc","1faf1-1f3fd","1faf1-1f3fe","1faf1-1f3ff"]},{"n":["leftwards hand"],"u":"1faf2","v":["1faf2-1f3fb","1faf2-1f3fc","1faf2-1f3fd","1faf2-1f3fe","1faf2-1f3ff"]},{"n":["palm down hand"],"u":"1faf3","v":["1faf3-1f3fb","1faf3-1f3fc","1faf3-1f3fd","1faf3-1f3fe","1faf3-1f3ff"]},{"n":["palm up hand"],"u":"1faf4","v":["1faf4-1f3fb","1faf4-1f3fc","1faf4-1f3fd","1faf4-1f3fe","1faf4-1f3ff"]},{"n":["ok hand","ok hand sign"],"u":"1f44c","v":["1f44c-1f3fb","1f44c-1f3fc","1f44c-1f3fd","1f44c-1f3fe","1f44c-1f3ff"]},{"n":["pinched fingers"],"u":"1f90c","v":["1f90c-1f3fb","1f90c-1f3fc","1f90c-1f3fd","1f90c-1f3fe","1f90c-1f3ff"]},{"n":["pinching hand"],"u":"1f90f","v":["1f90f-1f3fb","1f90f-1f3fc","1f90f-1f3fd","1f90f-1f3fe","1f90f-1f3ff"]},{"n":["v","victory hand"],"u":"270c-fe0f","v":["270c-1f3fb","270c-1f3fc","270c-1f3fd","270c-1f3fe","270c-1f3ff"]},{"n":["crossed fingers","hand with index and middle fingers crossed"],"u":"1f91e","v":["1f91e-1f3fb","1f91e-1f3fc","1f91e-1f3fd","1f91e-1f3fe","1f91e-1f3ff"]},{"n":["hand with index finger and thumb crossed"],"u":"1faf0","v":["1faf0-1f3fb","1faf0-1f3fc","1faf0-1f3fd","1faf0-1f3fe","1faf0-1f3ff"]},{"n":["i love you hand sign"],"u":"1f91f","v":["1f91f-1f3fb","1f91f-1f3fc","1f91f-1f3fd","1f91f-1f3fe","1f91f-1f3ff"]},{"n":["the horns","sign of the horns"],"u":"1f918","v":["1f918-1f3fb","1f918-1f3fc","1f918-1f3fd","1f918-1f3fe","1f918-1f3ff"]},{"n":["call me hand"],"u":"1f919","v":["1f919-1f3fb","1f919-1f3fc","1f919-1f3fd","1f919-1f3fe","1f919-1f3ff"]},{"n":["point left","white left pointing backhand index"],"u":"1f448","v":["1f448-1f3fb","1f448-1f3fc","1f448-1f3fd","1f448-1f3fe","1f448-1f3ff"]},{"n":["point right","white right pointing backhand index"],"u":"1f449","v":["1f449-1f3fb","1f449-1f3fc","1f449-1f3fd","1f449-1f3fe","1f449-1f3ff"]},{"n":["point up 2","white up pointing backhand index"],"u":"1f446","v":["1f446-1f3fb","1f446-1f3fc","1f446-1f3fd","1f446-1f3fe","1f446-1f3ff"]},{"n":["middle finger","reversed hand with middle finger extended"],"u":"1f595","v":["1f595-1f3fb","1f595-1f3fc","1f595-1f3fd","1f595-1f3fe","1f595-1f3ff"]},{"n":["point down","white down pointing backhand index"],"u":"1f447","v":["1f447-1f3fb","1f447-1f3fc","1f447-1f3fd","1f447-1f3fe","1f447-1f3ff"]},{"n":["point up","white up pointing index"],"u":"261d-fe0f","v":["261d-1f3fb","261d-1f3fc","261d-1f3fd","261d-1f3fe","261d-1f3ff"]},{"n":["index pointing at the viewer"],"u":"1faf5","v":["1faf5-1f3fb","1faf5-1f3fc","1faf5-1f3fd","1faf5-1f3fe","1faf5-1f3ff"]},{"n":["+1","thumbsup","thumbs up sign"],"u":"1f44d","v":["1f44d-1f3fb","1f44d-1f3fc","1f44d-1f3fd","1f44d-1f3fe","1f44d-1f3ff"]},{"n":["-1","thumbsdown","thumbs down sign"],"u":"1f44e","v":["1f44e-1f3fb","1f44e-1f3fc","1f44e-1f3fd","1f44e-1f3fe","1f44e-1f3ff"]},{"n":["fist","raised fist"],"u":"270a","v":["270a-1f3fb","270a-1f3fc","270a-1f3fd","270a-1f3fe","270a-1f3ff"]},{"n":["punch","facepunch","fisted hand sign"],"u":"1f44a","v":["1f44a-1f3fb","1f44a-1f3fc","1f44a-1f3fd","1f44a-1f3fe","1f44a-1f3ff"]},{"n":["left-facing fist"],"u":"1f91b","v":["1f91b-1f3fb","1f91b-1f3fc","1f91b-1f3fd","1f91b-1f3fe","1f91b-1f3ff"]},{"n":["right-facing fist"],"u":"1f91c","v":["1f91c-1f3fb","1f91c-1f3fc","1f91c-1f3fd","1f91c-1f3fe","1f91c-1f3ff"]},{"n":["clap","clapping hands sign"],"u":"1f44f","v":["1f44f-1f3fb","1f44f-1f3fc","1f44f-1f3fd","1f44f-1f3fe","1f44f-1f3ff"]},{"n":["raised hands","person raising both hands in celebration"],"u":"1f64c","v":["1f64c-1f3fb","1f64c-1f3fc","1f64c-1f3fd","1f64c-1f3fe","1f64c-1f3ff"]},{"n":["heart hands"],"u":"1faf6","v":["1faf6-1f3fb","1faf6-1f3fc","1faf6-1f3fd","1faf6-1f3fe","1faf6-1f3ff"]},{"n":["open hands","open hands sign"],"u":"1f450","v":["1f450-1f3fb","1f450-1f3fc","1f450-1f3fd","1f450-1f3fe","1f450-1f3ff"]},{"n":["palms up together"],"u":"1f932","v":["1f932-1f3fb","1f932-1f3fc","1f932-1f3fd","1f932-1f3fe","1f932-1f3ff"]},{"n":["handshake"],"u":"1f91d","v":["1f91d-1f3fb","1f91d-1f3fc","1f91d-1f3fd","1f91d-1f3fe","1f91d-1f3ff","1faf1-1f3fb-200d-1faf2-1f3fc","1faf1-1f3fb-200d-1faf2-1f3fd","1faf1-1f3fb-200d-1faf2-1f3fe","1faf1-1f3fb-200d-1faf2-1f3ff","1faf1-1f3fc-200d-1faf2-1f3fb","1faf1-1f3fc-200d-1faf2-1f3fd","1faf1-1f3fc-200d-1faf2-1f3fe","1faf1-1f3fc-200d-1faf2-1f3ff","1faf1-1f3fd-200d-1faf2-1f3fb","1faf1-1f3fd-200d-1faf2-1f3fc","1faf1-1f3fd-200d-1faf2-1f3fe","1faf1-1f3fd-200d-1faf2-1f3ff","1faf1-1f3fe-200d-1faf2-1f3fb","1faf1-1f3fe-200d-1faf2-1f3fc","1faf1-1f3fe-200d-1faf2-1f3fd","1faf1-1f3fe-200d-1faf2-1f3ff","1faf1-1f3ff-200d-1faf2-1f3fb","1faf1-1f3ff-200d-1faf2-1f3fc","1faf1-1f3ff-200d-1faf2-1f3fd","1faf1-1f3ff-200d-1faf2-1f3fe"]},{"n":["pray","person with folded hands"],"u":"1f64f","v":["1f64f-1f3fb","1f64f-1f3fc","1f64f-1f3fd","1f64f-1f3fe","1f64f-1f3ff"]},{"n":["writing hand"],"u":"270d-fe0f","v":["270d-1f3fb","270d-1f3fc","270d-1f3fd","270d-1f3fe","270d-1f3ff"]},{"n":["nail care","nail polish"],"u":"1f485","v":["1f485-1f3fb","1f485-1f3fc","1f485-1f3fd","1f485-1f3fe","1f485-1f3ff"]},{"n":["selfie"],"u":"1f933","v":["1f933-1f3fb","1f933-1f3fc","1f933-1f3fd","1f933-1f3fe","1f933-1f3ff"]},{"n":["muscle","flexed biceps"],"u":"1f4aa","v":["1f4aa-1f3fb","1f4aa-1f3fc","1f4aa-1f3fd","1f4aa-1f3fe","1f4aa-1f3ff"]},{"n":["mechanical arm"],"u":"1f9be"},{"n":["mechanical leg"],"u":"1f9bf"},{"n":["leg"],"u":"1f9b5","v":["1f9b5-1f3fb","1f9b5-1f3fc","1f9b5-1f3fd","1f9b5-1f3fe","1f9b5-1f3ff"]},{"n":["foot"],"u":"1f9b6","v":["1f9b6-1f3fb","1f9b6-1f3fc","1f9b6-1f3fd","1f9b6-1f3fe","1f9b6-1f3ff"]},{"n":["ear"],"u":"1f442","v":["1f442-1f3fb","1f442-1f3fc","1f442-1f3fd","1f442-1f3fe","1f442-1f3ff"]},{"n":["ear with hearing aid"],"u":"1f9bb","v":["1f9bb-1f3fb","1f9bb-1f3fc","1f9bb-1f3fd","1f9bb-1f3fe","1f9bb-1f3ff"]},{"n":["nose"],"u":"1f443","v":["1f443-1f3fb","1f443-1f3fc","1f443-1f3fd","1f443-1f3fe","1f443-1f3ff"]},{"n":["brain"],"u":"1f9e0"},{"n":["anatomical heart"],"u":"1fac0"},{"n":["lungs"],"u":"1fac1"},{"n":["tooth"],"u":"1f9b7"},{"n":["bone"],"u":"1f9b4"},{"n":["eyes"],"u":"1f440"},{"n":["eye"],"u":"1f441-fe0f"},{"n":["tongue"],"u":"1f445"},{"n":["lips","mouth"],"u":"1f444"},{"n":["biting lip"],"u":"1fae6"},{"n":["baby"],"u":"1f476","v":["1f476-1f3fb","1f476-1f3fc","1f476-1f3fd","1f476-1f3fe","1f476-1f3ff"]},{"n":["child"],"u":"1f9d2","v":["1f9d2-1f3fb","1f9d2-1f3fc","1f9d2-1f3fd","1f9d2-1f3fe","1f9d2-1f3ff"]},{"n":["boy"],"u":"1f466","v":["1f466-1f3fb","1f466-1f3fc","1f466-1f3fd","1f466-1f3fe","1f466-1f3ff"]},{"n":["girl"],"u":"1f467","v":["1f467-1f3fb","1f467-1f3fc","1f467-1f3fd","1f467-1f3fe","1f467-1f3ff"]},{"n":["adult"],"u":"1f9d1","v":["1f9d1-1f3fb","1f9d1-1f3fc","1f9d1-1f3fd","1f9d1-1f3fe","1f9d1-1f3ff"]},{"n":["person with blond hair"],"u":"1f471","v":["1f471-1f3fb","1f471-1f3fc","1f471-1f3fd","1f471-1f3fe","1f471-1f3ff"]},{"n":["man"],"u":"1f468","v":["1f468-1f3fb","1f468-1f3fc","1f468-1f3fd","1f468-1f3fe","1f468-1f3ff"]},{"n":["bearded person"],"u":"1f9d4","v":["1f9d4-1f3fb","1f9d4-1f3fc","1f9d4-1f3fd","1f9d4-1f3fe","1f9d4-1f3ff"]},{"n":["man: beard","man with beard"],"u":"1f9d4-200d-2642-fe0f","v":["1f9d4-1f3fb-200d-2642-fe0f","1f9d4-1f3fc-200d-2642-fe0f","1f9d4-1f3fd-200d-2642-fe0f","1f9d4-1f3fe-200d-2642-fe0f","1f9d4-1f3ff-200d-2642-fe0f"]},{"n":["woman: beard","woman with beard"],"u":"1f9d4-200d-2640-fe0f","v":["1f9d4-1f3fb-200d-2640-fe0f","1f9d4-1f3fc-200d-2640-fe0f","1f9d4-1f3fd-200d-2640-fe0f","1f9d4-1f3fe-200d-2640-fe0f","1f9d4-1f3ff-200d-2640-fe0f"]},{"n":["man: red hair","red haired man"],"u":"1f468-200d-1f9b0","v":["1f468-1f3fb-200d-1f9b0","1f468-1f3fc-200d-1f9b0","1f468-1f3fd-200d-1f9b0","1f468-1f3fe-200d-1f9b0","1f468-1f3ff-200d-1f9b0"]},{"n":["man: curly hair","curly haired man"],"u":"1f468-200d-1f9b1","v":["1f468-1f3fb-200d-1f9b1","1f468-1f3fc-200d-1f9b1","1f468-1f3fd-200d-1f9b1","1f468-1f3fe-200d-1f9b1","1f468-1f3ff-200d-1f9b1"]},{"n":["man: white hair","white haired man"],"u":"1f468-200d-1f9b3","v":["1f468-1f3fb-200d-1f9b3","1f468-1f3fc-200d-1f9b3","1f468-1f3fd-200d-1f9b3","1f468-1f3fe-200d-1f9b3","1f468-1f3ff-200d-1f9b3"]},{"n":["bald man","man: bald"],"u":"1f468-200d-1f9b2","v":["1f468-1f3fb-200d-1f9b2","1f468-1f3fc-200d-1f9b2","1f468-1f3fd-200d-1f9b2","1f468-1f3fe-200d-1f9b2","1f468-1f3ff-200d-1f9b2"]},{"n":["woman"],"u":"1f469","v":["1f469-1f3fb","1f469-1f3fc","1f469-1f3fd","1f469-1f3fe","1f469-1f3ff"]},{"n":["woman: red hair","red haired woman"],"u":"1f469-200d-1f9b0","v":["1f469-1f3fb-200d-1f9b0","1f469-1f3fc-200d-1f9b0","1f469-1f3fd-200d-1f9b0","1f469-1f3fe-200d-1f9b0","1f469-1f3ff-200d-1f9b0"]},{"n":["person: red hair","red haired person"],"u":"1f9d1-200d-1f9b0","v":["1f9d1-1f3fb-200d-1f9b0","1f9d1-1f3fc-200d-1f9b0","1f9d1-1f3fd-200d-1f9b0","1f9d1-1f3fe-200d-1f9b0","1f9d1-1f3ff-200d-1f9b0"]},{"n":["woman: curly hair","curly haired woman"],"u":"1f469-200d-1f9b1","v":["1f469-1f3fb-200d-1f9b1","1f469-1f3fc-200d-1f9b1","1f469-1f3fd-200d-1f9b1","1f469-1f3fe-200d-1f9b1","1f469-1f3ff-200d-1f9b1"]},{"n":["person: curly hair","curly haired person"],"u":"1f9d1-200d-1f9b1","v":["1f9d1-1f3fb-200d-1f9b1","1f9d1-1f3fc-200d-1f9b1","1f9d1-1f3fd-200d-1f9b1","1f9d1-1f3fe-200d-1f9b1","1f9d1-1f3ff-200d-1f9b1"]},{"n":["woman: white hair","white haired woman"],"u":"1f469-200d-1f9b3","v":["1f469-1f3fb-200d-1f9b3","1f469-1f3fc-200d-1f9b3","1f469-1f3fd-200d-1f9b3","1f469-1f3fe-200d-1f9b3","1f469-1f3ff-200d-1f9b3"]},{"n":["person: white hair","white haired person"],"u":"1f9d1-200d-1f9b3","v":["1f9d1-1f3fb-200d-1f9b3","1f9d1-1f3fc-200d-1f9b3","1f9d1-1f3fd-200d-1f9b3","1f9d1-1f3fe-200d-1f9b3","1f9d1-1f3ff-200d-1f9b3"]},{"n":["bald woman","woman: bald"],"u":"1f469-200d-1f9b2","v":["1f469-1f3fb-200d-1f9b2","1f469-1f3fc-200d-1f9b2","1f469-1f3fd-200d-1f9b2","1f469-1f3fe-200d-1f9b2","1f469-1f3ff-200d-1f9b2"]},{"n":["bald person","person: bald"],"u":"1f9d1-200d-1f9b2","v":["1f9d1-1f3fb-200d-1f9b2","1f9d1-1f3fc-200d-1f9b2","1f9d1-1f3fd-200d-1f9b2","1f9d1-1f3fe-200d-1f9b2","1f9d1-1f3ff-200d-1f9b2"]},{"n":["woman: blond hair","blond-haired-woman"],"u":"1f471-200d-2640-fe0f","v":["1f471-1f3fb-200d-2640-fe0f","1f471-1f3fc-200d-2640-fe0f","1f471-1f3fd-200d-2640-fe0f","1f471-1f3fe-200d-2640-fe0f","1f471-1f3ff-200d-2640-fe0f"]},{"n":["man: blond hair","blond-haired-man"],"u":"1f471-200d-2642-fe0f","v":["1f471-1f3fb-200d-2642-fe0f","1f471-1f3fc-200d-2642-fe0f","1f471-1f3fd-200d-2642-fe0f","1f471-1f3fe-200d-2642-fe0f","1f471-1f3ff-200d-2642-fe0f"]},{"n":["older adult"],"u":"1f9d3","v":["1f9d3-1f3fb","1f9d3-1f3fc","1f9d3-1f3fd","1f9d3-1f3fe","1f9d3-1f3ff"]},{"n":["older man"],"u":"1f474","v":["1f474-1f3fb","1f474-1f3fc","1f474-1f3fd","1f474-1f3fe","1f474-1f3ff"]},{"n":["older woman"],"u":"1f475","v":["1f475-1f3fb","1f475-1f3fc","1f475-1f3fd","1f475-1f3fe","1f475-1f3ff"]},{"n":["person frowning"],"u":"1f64d","v":["1f64d-1f3fb","1f64d-1f3fc","1f64d-1f3fd","1f64d-1f3fe","1f64d-1f3ff"]},{"n":["man frowning","man-frowning"],"u":"1f64d-200d-2642-fe0f","v":["1f64d-1f3fb-200d-2642-fe0f","1f64d-1f3fc-200d-2642-fe0f","1f64d-1f3fd-200d-2642-fe0f","1f64d-1f3fe-200d-2642-fe0f","1f64d-1f3ff-200d-2642-fe0f"]},{"n":["woman frowning","woman-frowning"],"u":"1f64d-200d-2640-fe0f","v":["1f64d-1f3fb-200d-2640-fe0f","1f64d-1f3fc-200d-2640-fe0f","1f64d-1f3fd-200d-2640-fe0f","1f64d-1f3fe-200d-2640-fe0f","1f64d-1f3ff-200d-2640-fe0f"]},{"n":["person with pouting face"],"u":"1f64e","v":["1f64e-1f3fb","1f64e-1f3fc","1f64e-1f3fd","1f64e-1f3fe","1f64e-1f3ff"]},{"n":["man pouting","man-pouting"],"u":"1f64e-200d-2642-fe0f","v":["1f64e-1f3fb-200d-2642-fe0f","1f64e-1f3fc-200d-2642-fe0f","1f64e-1f3fd-200d-2642-fe0f","1f64e-1f3fe-200d-2642-fe0f","1f64e-1f3ff-200d-2642-fe0f"]},{"n":["woman pouting","woman-pouting"],"u":"1f64e-200d-2640-fe0f","v":["1f64e-1f3fb-200d-2640-fe0f","1f64e-1f3fc-200d-2640-fe0f","1f64e-1f3fd-200d-2640-fe0f","1f64e-1f3fe-200d-2640-fe0f","1f64e-1f3ff-200d-2640-fe0f"]},{"n":["no good","face with no good gesture"],"u":"1f645","v":["1f645-1f3fb","1f645-1f3fc","1f645-1f3fd","1f645-1f3fe","1f645-1f3ff"]},{"n":["man gesturing no","man-gesturing-no"],"u":"1f645-200d-2642-fe0f","v":["1f645-1f3fb-200d-2642-fe0f","1f645-1f3fc-200d-2642-fe0f","1f645-1f3fd-200d-2642-fe0f","1f645-1f3fe-200d-2642-fe0f","1f645-1f3ff-200d-2642-fe0f"]},{"n":["woman gesturing no","woman-gesturing-no"],"u":"1f645-200d-2640-fe0f","v":["1f645-1f3fb-200d-2640-fe0f","1f645-1f3fc-200d-2640-fe0f","1f645-1f3fd-200d-2640-fe0f","1f645-1f3fe-200d-2640-fe0f","1f645-1f3ff-200d-2640-fe0f"]},{"n":["ok woman","face with ok gesture"],"u":"1f646","v":["1f646-1f3fb","1f646-1f3fc","1f646-1f3fd","1f646-1f3fe","1f646-1f3ff"]},{"n":["man gesturing ok","man-gesturing-ok"],"u":"1f646-200d-2642-fe0f","v":["1f646-1f3fb-200d-2642-fe0f","1f646-1f3fc-200d-2642-fe0f","1f646-1f3fd-200d-2642-fe0f","1f646-1f3fe-200d-2642-fe0f","1f646-1f3ff-200d-2642-fe0f"]},{"n":["woman gesturing ok","woman-gesturing-ok"],"u":"1f646-200d-2640-fe0f","v":["1f646-1f3fb-200d-2640-fe0f","1f646-1f3fc-200d-2640-fe0f","1f646-1f3fd-200d-2640-fe0f","1f646-1f3fe-200d-2640-fe0f","1f646-1f3ff-200d-2640-fe0f"]},{"n":["information desk person"],"u":"1f481","v":["1f481-1f3fb","1f481-1f3fc","1f481-1f3fd","1f481-1f3fe","1f481-1f3ff"]},{"n":["man tipping hand","man-tipping-hand"],"u":"1f481-200d-2642-fe0f","v":["1f481-1f3fb-200d-2642-fe0f","1f481-1f3fc-200d-2642-fe0f","1f481-1f3fd-200d-2642-fe0f","1f481-1f3fe-200d-2642-fe0f","1f481-1f3ff-200d-2642-fe0f"]},{"n":["woman tipping hand","woman-tipping-hand"],"u":"1f481-200d-2640-fe0f","v":["1f481-1f3fb-200d-2640-fe0f","1f481-1f3fc-200d-2640-fe0f","1f481-1f3fd-200d-2640-fe0f","1f481-1f3fe-200d-2640-fe0f","1f481-1f3ff-200d-2640-fe0f"]},{"n":["raising hand","happy person raising one hand"],"u":"1f64b","v":["1f64b-1f3fb","1f64b-1f3fc","1f64b-1f3fd","1f64b-1f3fe","1f64b-1f3ff"]},{"n":["man raising hand","man-raising-hand"],"u":"1f64b-200d-2642-fe0f","v":["1f64b-1f3fb-200d-2642-fe0f","1f64b-1f3fc-200d-2642-fe0f","1f64b-1f3fd-200d-2642-fe0f","1f64b-1f3fe-200d-2642-fe0f","1f64b-1f3ff-200d-2642-fe0f"]},{"n":["woman raising hand","woman-raising-hand"],"u":"1f64b-200d-2640-fe0f","v":["1f64b-1f3fb-200d-2640-fe0f","1f64b-1f3fc-200d-2640-fe0f","1f64b-1f3fd-200d-2640-fe0f","1f64b-1f3fe-200d-2640-fe0f","1f64b-1f3ff-200d-2640-fe0f"]},{"n":["deaf person"],"u":"1f9cf","v":["1f9cf-1f3fb","1f9cf-1f3fc","1f9cf-1f3fd","1f9cf-1f3fe","1f9cf-1f3ff"]},{"n":["deaf man"],"u":"1f9cf-200d-2642-fe0f","v":["1f9cf-1f3fb-200d-2642-fe0f","1f9cf-1f3fc-200d-2642-fe0f","1f9cf-1f3fd-200d-2642-fe0f","1f9cf-1f3fe-200d-2642-fe0f","1f9cf-1f3ff-200d-2642-fe0f"]},{"n":["deaf woman"],"u":"1f9cf-200d-2640-fe0f","v":["1f9cf-1f3fb-200d-2640-fe0f","1f9cf-1f3fc-200d-2640-fe0f","1f9cf-1f3fd-200d-2640-fe0f","1f9cf-1f3fe-200d-2640-fe0f","1f9cf-1f3ff-200d-2640-fe0f"]},{"n":["bow","person bowing deeply"],"u":"1f647","v":["1f647-1f3fb","1f647-1f3fc","1f647-1f3fd","1f647-1f3fe","1f647-1f3ff"]},{"n":["man bowing","man-bowing"],"u":"1f647-200d-2642-fe0f","v":["1f647-1f3fb-200d-2642-fe0f","1f647-1f3fc-200d-2642-fe0f","1f647-1f3fd-200d-2642-fe0f","1f647-1f3fe-200d-2642-fe0f","1f647-1f3ff-200d-2642-fe0f"]},{"n":["woman bowing","woman-bowing"],"u":"1f647-200d-2640-fe0f","v":["1f647-1f3fb-200d-2640-fe0f","1f647-1f3fc-200d-2640-fe0f","1f647-1f3fd-200d-2640-fe0f","1f647-1f3fe-200d-2640-fe0f","1f647-1f3ff-200d-2640-fe0f"]},{"n":["face palm"],"u":"1f926","v":["1f926-1f3fb","1f926-1f3fc","1f926-1f3fd","1f926-1f3fe","1f926-1f3ff"]},{"n":["man facepalming","man-facepalming"],"u":"1f926-200d-2642-fe0f","v":["1f926-1f3fb-200d-2642-fe0f","1f926-1f3fc-200d-2642-fe0f","1f926-1f3fd-200d-2642-fe0f","1f926-1f3fe-200d-2642-fe0f","1f926-1f3ff-200d-2642-fe0f"]},{"n":["woman facepalming","woman-facepalming"],"u":"1f926-200d-2640-fe0f","v":["1f926-1f3fb-200d-2640-fe0f","1f926-1f3fc-200d-2640-fe0f","1f926-1f3fd-200d-2640-fe0f","1f926-1f3fe-200d-2640-fe0f","1f926-1f3ff-200d-2640-fe0f"]},{"n":["shrug"],"u":"1f937","v":["1f937-1f3fb","1f937-1f3fc","1f937-1f3fd","1f937-1f3fe","1f937-1f3ff"]},{"n":["man shrugging","man-shrugging"],"u":"1f937-200d-2642-fe0f","v":["1f937-1f3fb-200d-2642-fe0f","1f937-1f3fc-200d-2642-fe0f","1f937-1f3fd-200d-2642-fe0f","1f937-1f3fe-200d-2642-fe0f","1f937-1f3ff-200d-2642-fe0f"]},{"n":["woman shrugging","woman-shrugging"],"u":"1f937-200d-2640-fe0f","v":["1f937-1f3fb-200d-2640-fe0f","1f937-1f3fc-200d-2640-fe0f","1f937-1f3fd-200d-2640-fe0f","1f937-1f3fe-200d-2640-fe0f","1f937-1f3ff-200d-2640-fe0f"]},{"n":["health worker"],"u":"1f9d1-200d-2695-fe0f","v":["1f9d1-1f3fb-200d-2695-fe0f","1f9d1-1f3fc-200d-2695-fe0f","1f9d1-1f3fd-200d-2695-fe0f","1f9d1-1f3fe-200d-2695-fe0f","1f9d1-1f3ff-200d-2695-fe0f"]},{"n":["male-doctor","man health worker"],"u":"1f468-200d-2695-fe0f","v":["1f468-1f3fb-200d-2695-fe0f","1f468-1f3fc-200d-2695-fe0f","1f468-1f3fd-200d-2695-fe0f","1f468-1f3fe-200d-2695-fe0f","1f468-1f3ff-200d-2695-fe0f"]},{"n":["female-doctor","woman health worker"],"u":"1f469-200d-2695-fe0f","v":["1f469-1f3fb-200d-2695-fe0f","1f469-1f3fc-200d-2695-fe0f","1f469-1f3fd-200d-2695-fe0f","1f469-1f3fe-200d-2695-fe0f","1f469-1f3ff-200d-2695-fe0f"]},{"n":["student"],"u":"1f9d1-200d-1f393","v":["1f9d1-1f3fb-200d-1f393","1f9d1-1f3fc-200d-1f393","1f9d1-1f3fd-200d-1f393","1f9d1-1f3fe-200d-1f393","1f9d1-1f3ff-200d-1f393"]},{"n":["man student","male-student"],"u":"1f468-200d-1f393","v":["1f468-1f3fb-200d-1f393","1f468-1f3fc-200d-1f393","1f468-1f3fd-200d-1f393","1f468-1f3fe-200d-1f393","1f468-1f3ff-200d-1f393"]},{"n":["woman student","female-student"],"u":"1f469-200d-1f393","v":["1f469-1f3fb-200d-1f393","1f469-1f3fc-200d-1f393","1f469-1f3fd-200d-1f393","1f469-1f3fe-200d-1f393","1f469-1f3ff-200d-1f393"]},{"n":["teacher"],"u":"1f9d1-200d-1f3eb","v":["1f9d1-1f3fb-200d-1f3eb","1f9d1-1f3fc-200d-1f3eb","1f9d1-1f3fd-200d-1f3eb","1f9d1-1f3fe-200d-1f3eb","1f9d1-1f3ff-200d-1f3eb"]},{"n":["man teacher","male-teacher"],"u":"1f468-200d-1f3eb","v":["1f468-1f3fb-200d-1f3eb","1f468-1f3fc-200d-1f3eb","1f468-1f3fd-200d-1f3eb","1f468-1f3fe-200d-1f3eb","1f468-1f3ff-200d-1f3eb"]},{"n":["woman teacher","female-teacher"],"u":"1f469-200d-1f3eb","v":["1f469-1f3fb-200d-1f3eb","1f469-1f3fc-200d-1f3eb","1f469-1f3fd-200d-1f3eb","1f469-1f3fe-200d-1f3eb","1f469-1f3ff-200d-1f3eb"]},{"n":["judge"],"u":"1f9d1-200d-2696-fe0f","v":["1f9d1-1f3fb-200d-2696-fe0f","1f9d1-1f3fc-200d-2696-fe0f","1f9d1-1f3fd-200d-2696-fe0f","1f9d1-1f3fe-200d-2696-fe0f","1f9d1-1f3ff-200d-2696-fe0f"]},{"n":["man judge","male-judge"],"u":"1f468-200d-2696-fe0f","v":["1f468-1f3fb-200d-2696-fe0f","1f468-1f3fc-200d-2696-fe0f","1f468-1f3fd-200d-2696-fe0f","1f468-1f3fe-200d-2696-fe0f","1f468-1f3ff-200d-2696-fe0f"]},{"n":["woman judge","female-judge"],"u":"1f469-200d-2696-fe0f","v":["1f469-1f3fb-200d-2696-fe0f","1f469-1f3fc-200d-2696-fe0f","1f469-1f3fd-200d-2696-fe0f","1f469-1f3fe-200d-2696-fe0f","1f469-1f3ff-200d-2696-fe0f"]},{"n":["farmer"],"u":"1f9d1-200d-1f33e","v":["1f9d1-1f3fb-200d-1f33e","1f9d1-1f3fc-200d-1f33e","1f9d1-1f3fd-200d-1f33e","1f9d1-1f3fe-200d-1f33e","1f9d1-1f3ff-200d-1f33e"]},{"n":["man farmer","male-farmer"],"u":"1f468-200d-1f33e","v":["1f468-1f3fb-200d-1f33e","1f468-1f3fc-200d-1f33e","1f468-1f3fd-200d-1f33e","1f468-1f3fe-200d-1f33e","1f468-1f3ff-200d-1f33e"]},{"n":["woman farmer","female-farmer"],"u":"1f469-200d-1f33e","v":["1f469-1f3fb-200d-1f33e","1f469-1f3fc-200d-1f33e","1f469-1f3fd-200d-1f33e","1f469-1f3fe-200d-1f33e","1f469-1f3ff-200d-1f33e"]},{"n":["cook"],"u":"1f9d1-200d-1f373","v":["1f9d1-1f3fb-200d-1f373","1f9d1-1f3fc-200d-1f373","1f9d1-1f3fd-200d-1f373","1f9d1-1f3fe-200d-1f373","1f9d1-1f3ff-200d-1f373"]},{"n":["man cook","male-cook"],"u":"1f468-200d-1f373","v":["1f468-1f3fb-200d-1f373","1f468-1f3fc-200d-1f373","1f468-1f3fd-200d-1f373","1f468-1f3fe-200d-1f373","1f468-1f3ff-200d-1f373"]},{"n":["woman cook","female-cook"],"u":"1f469-200d-1f373","v":["1f469-1f3fb-200d-1f373","1f469-1f3fc-200d-1f373","1f469-1f3fd-200d-1f373","1f469-1f3fe-200d-1f373","1f469-1f3ff-200d-1f373"]},{"n":["mechanic"],"u":"1f9d1-200d-1f527","v":["1f9d1-1f3fb-200d-1f527","1f9d1-1f3fc-200d-1f527","1f9d1-1f3fd-200d-1f527","1f9d1-1f3fe-200d-1f527","1f9d1-1f3ff-200d-1f527"]},{"n":["man mechanic","male-mechanic"],"u":"1f468-200d-1f527","v":["1f468-1f3fb-200d-1f527","1f468-1f3fc-200d-1f527","1f468-1f3fd-200d-1f527","1f468-1f3fe-200d-1f527","1f468-1f3ff-200d-1f527"]},{"n":["woman mechanic","female-mechanic"],"u":"1f469-200d-1f527","v":["1f469-1f3fb-200d-1f527","1f469-1f3fc-200d-1f527","1f469-1f3fd-200d-1f527","1f469-1f3fe-200d-1f527","1f469-1f3ff-200d-1f527"]},{"n":["factory worker"],"u":"1f9d1-200d-1f3ed","v":["1f9d1-1f3fb-200d-1f3ed","1f9d1-1f3fc-200d-1f3ed","1f9d1-1f3fd-200d-1f3ed","1f9d1-1f3fe-200d-1f3ed","1f9d1-1f3ff-200d-1f3ed"]},{"n":["man factory worker","male-factory-worker"],"u":"1f468-200d-1f3ed","v":["1f468-1f3fb-200d-1f3ed","1f468-1f3fc-200d-1f3ed","1f468-1f3fd-200d-1f3ed","1f468-1f3fe-200d-1f3ed","1f468-1f3ff-200d-1f3ed"]},{"n":["woman factory worker","female-factory-worker"],"u":"1f469-200d-1f3ed","v":["1f469-1f3fb-200d-1f3ed","1f469-1f3fc-200d-1f3ed","1f469-1f3fd-200d-1f3ed","1f469-1f3fe-200d-1f3ed","1f469-1f3ff-200d-1f3ed"]},{"n":["office worker"],"u":"1f9d1-200d-1f4bc","v":["1f9d1-1f3fb-200d-1f4bc","1f9d1-1f3fc-200d-1f4bc","1f9d1-1f3fd-200d-1f4bc","1f9d1-1f3fe-200d-1f4bc","1f9d1-1f3ff-200d-1f4bc"]},{"n":["man office worker","male-office-worker"],"u":"1f468-200d-1f4bc","v":["1f468-1f3fb-200d-1f4bc","1f468-1f3fc-200d-1f4bc","1f468-1f3fd-200d-1f4bc","1f468-1f3fe-200d-1f4bc","1f468-1f3ff-200d-1f4bc"]},{"n":["woman office worker","female-office-worker"],"u":"1f469-200d-1f4bc","v":["1f469-1f3fb-200d-1f4bc","1f469-1f3fc-200d-1f4bc","1f469-1f3fd-200d-1f4bc","1f469-1f3fe-200d-1f4bc","1f469-1f3ff-200d-1f4bc"]},{"n":["scientist"],"u":"1f9d1-200d-1f52c","v":["1f9d1-1f3fb-200d-1f52c","1f9d1-1f3fc-200d-1f52c","1f9d1-1f3fd-200d-1f52c","1f9d1-1f3fe-200d-1f52c","1f9d1-1f3ff-200d-1f52c"]},{"n":["man scientist","male-scientist"],"u":"1f468-200d-1f52c","v":["1f468-1f3fb-200d-1f52c","1f468-1f3fc-200d-1f52c","1f468-1f3fd-200d-1f52c","1f468-1f3fe-200d-1f52c","1f468-1f3ff-200d-1f52c"]},{"n":["woman scientist","female-scientist"],"u":"1f469-200d-1f52c","v":["1f469-1f3fb-200d-1f52c","1f469-1f3fc-200d-1f52c","1f469-1f3fd-200d-1f52c","1f469-1f3fe-200d-1f52c","1f469-1f3ff-200d-1f52c"]},{"n":["technologist"],"u":"1f9d1-200d-1f4bb","v":["1f9d1-1f3fb-200d-1f4bb","1f9d1-1f3fc-200d-1f4bb","1f9d1-1f3fd-200d-1f4bb","1f9d1-1f3fe-200d-1f4bb","1f9d1-1f3ff-200d-1f4bb"]},{"n":["man technologist","male-technologist"],"u":"1f468-200d-1f4bb","v":["1f468-1f3fb-200d-1f4bb","1f468-1f3fc-200d-1f4bb","1f468-1f3fd-200d-1f4bb","1f468-1f3fe-200d-1f4bb","1f468-1f3ff-200d-1f4bb"]},{"n":["woman technologist","female-technologist"],"u":"1f469-200d-1f4bb","v":["1f469-1f3fb-200d-1f4bb","1f469-1f3fc-200d-1f4bb","1f469-1f3fd-200d-1f4bb","1f469-1f3fe-200d-1f4bb","1f469-1f3ff-200d-1f4bb"]},{"n":["singer"],"u":"1f9d1-200d-1f3a4","v":["1f9d1-1f3fb-200d-1f3a4","1f9d1-1f3fc-200d-1f3a4","1f9d1-1f3fd-200d-1f3a4","1f9d1-1f3fe-200d-1f3a4","1f9d1-1f3ff-200d-1f3a4"]},{"n":["man singer","male-singer"],"u":"1f468-200d-1f3a4","v":["1f468-1f3fb-200d-1f3a4","1f468-1f3fc-200d-1f3a4","1f468-1f3fd-200d-1f3a4","1f468-1f3fe-200d-1f3a4","1f468-1f3ff-200d-1f3a4"]},{"n":["woman singer","female-singer"],"u":"1f469-200d-1f3a4","v":["1f469-1f3fb-200d-1f3a4","1f469-1f3fc-200d-1f3a4","1f469-1f3fd-200d-1f3a4","1f469-1f3fe-200d-1f3a4","1f469-1f3ff-200d-1f3a4"]},{"n":["artist"],"u":"1f9d1-200d-1f3a8","v":["1f9d1-1f3fb-200d-1f3a8","1f9d1-1f3fc-200d-1f3a8","1f9d1-1f3fd-200d-1f3a8","1f9d1-1f3fe-200d-1f3a8","1f9d1-1f3ff-200d-1f3a8"]},{"n":["man artist","male-artist"],"u":"1f468-200d-1f3a8","v":["1f468-1f3fb-200d-1f3a8","1f468-1f3fc-200d-1f3a8","1f468-1f3fd-200d-1f3a8","1f468-1f3fe-200d-1f3a8","1f468-1f3ff-200d-1f3a8"]},{"n":["woman artist","female-artist"],"u":"1f469-200d-1f3a8","v":["1f469-1f3fb-200d-1f3a8","1f469-1f3fc-200d-1f3a8","1f469-1f3fd-200d-1f3a8","1f469-1f3fe-200d-1f3a8","1f469-1f3ff-200d-1f3a8"]},{"n":["pilot"],"u":"1f9d1-200d-2708-fe0f","v":["1f9d1-1f3fb-200d-2708-fe0f","1f9d1-1f3fc-200d-2708-fe0f","1f9d1-1f3fd-200d-2708-fe0f","1f9d1-1f3fe-200d-2708-fe0f","1f9d1-1f3ff-200d-2708-fe0f"]},{"n":["man pilot","male-pilot"],"u":"1f468-200d-2708-fe0f","v":["1f468-1f3fb-200d-2708-fe0f","1f468-1f3fc-200d-2708-fe0f","1f468-1f3fd-200d-2708-fe0f","1f468-1f3fe-200d-2708-fe0f","1f468-1f3ff-200d-2708-fe0f"]},{"n":["woman pilot","female-pilot"],"u":"1f469-200d-2708-fe0f","v":["1f469-1f3fb-200d-2708-fe0f","1f469-1f3fc-200d-2708-fe0f","1f469-1f3fd-200d-2708-fe0f","1f469-1f3fe-200d-2708-fe0f","1f469-1f3ff-200d-2708-fe0f"]},{"n":["astronaut"],"u":"1f9d1-200d-1f680","v":["1f9d1-1f3fb-200d-1f680","1f9d1-1f3fc-200d-1f680","1f9d1-1f3fd-200d-1f680","1f9d1-1f3fe-200d-1f680","1f9d1-1f3ff-200d-1f680"]},{"n":["man astronaut","male-astronaut"],"u":"1f468-200d-1f680","v":["1f468-1f3fb-200d-1f680","1f468-1f3fc-200d-1f680","1f468-1f3fd-200d-1f680","1f468-1f3fe-200d-1f680","1f468-1f3ff-200d-1f680"]},{"n":["woman astronaut","female-astronaut"],"u":"1f469-200d-1f680","v":["1f469-1f3fb-200d-1f680","1f469-1f3fc-200d-1f680","1f469-1f3fd-200d-1f680","1f469-1f3fe-200d-1f680","1f469-1f3ff-200d-1f680"]},{"n":["firefighter"],"u":"1f9d1-200d-1f692","v":["1f9d1-1f3fb-200d-1f692","1f9d1-1f3fc-200d-1f692","1f9d1-1f3fd-200d-1f692","1f9d1-1f3fe-200d-1f692","1f9d1-1f3ff-200d-1f692"]},{"n":["man firefighter","male-firefighter"],"u":"1f468-200d-1f692","v":["1f468-1f3fb-200d-1f692","1f468-1f3fc-200d-1f692","1f468-1f3fd-200d-1f692","1f468-1f3fe-200d-1f692","1f468-1f3ff-200d-1f692"]},{"n":["woman firefighter","female-firefighter"],"u":"1f469-200d-1f692","v":["1f469-1f3fb-200d-1f692","1f469-1f3fc-200d-1f692","1f469-1f3fd-200d-1f692","1f469-1f3fe-200d-1f692","1f469-1f3ff-200d-1f692"]},{"n":["cop","police officer"],"u":"1f46e","v":["1f46e-1f3fb","1f46e-1f3fc","1f46e-1f3fd","1f46e-1f3fe","1f46e-1f3ff"]},{"n":["man police officer","male-police-officer"],"u":"1f46e-200d-2642-fe0f","v":["1f46e-1f3fb-200d-2642-fe0f","1f46e-1f3fc-200d-2642-fe0f","1f46e-1f3fd-200d-2642-fe0f","1f46e-1f3fe-200d-2642-fe0f","1f46e-1f3ff-200d-2642-fe0f"]},{"n":["woman police officer","female-police-officer"],"u":"1f46e-200d-2640-fe0f","v":["1f46e-1f3fb-200d-2640-fe0f","1f46e-1f3fc-200d-2640-fe0f","1f46e-1f3fd-200d-2640-fe0f","1f46e-1f3fe-200d-2640-fe0f","1f46e-1f3ff-200d-2640-fe0f"]},{"n":["detective","sleuth or spy"],"u":"1f575-fe0f","v":["1f575-1f3fb","1f575-1f3fc","1f575-1f3fd","1f575-1f3fe","1f575-1f3ff"]},{"n":["man detective","male-detective"],"u":"1f575-fe0f-200d-2642-fe0f","v":["1f575-1f3fb-200d-2642-fe0f","1f575-1f3fc-200d-2642-fe0f","1f575-1f3fd-200d-2642-fe0f","1f575-1f3fe-200d-2642-fe0f","1f575-1f3ff-200d-2642-fe0f"]},{"n":["woman detective","female-detective"],"u":"1f575-fe0f-200d-2640-fe0f","v":["1f575-1f3fb-200d-2640-fe0f","1f575-1f3fc-200d-2640-fe0f","1f575-1f3fd-200d-2640-fe0f","1f575-1f3fe-200d-2640-fe0f","1f575-1f3ff-200d-2640-fe0f"]},{"n":["guardsman"],"u":"1f482","v":["1f482-1f3fb","1f482-1f3fc","1f482-1f3fd","1f482-1f3fe","1f482-1f3ff"]},{"n":["man guard","male-guard"],"u":"1f482-200d-2642-fe0f","v":["1f482-1f3fb-200d-2642-fe0f","1f482-1f3fc-200d-2642-fe0f","1f482-1f3fd-200d-2642-fe0f","1f482-1f3fe-200d-2642-fe0f","1f482-1f3ff-200d-2642-fe0f"]},{"n":["woman guard","female-guard"],"u":"1f482-200d-2640-fe0f","v":["1f482-1f3fb-200d-2640-fe0f","1f482-1f3fc-200d-2640-fe0f","1f482-1f3fd-200d-2640-fe0f","1f482-1f3fe-200d-2640-fe0f","1f482-1f3ff-200d-2640-fe0f"]},{"n":["ninja"],"u":"1f977","v":["1f977-1f3fb","1f977-1f3fc","1f977-1f3fd","1f977-1f3fe","1f977-1f3ff"]},{"n":["construction worker"],"u":"1f477","v":["1f477-1f3fb","1f477-1f3fc","1f477-1f3fd","1f477-1f3fe","1f477-1f3ff"]},{"n":["man construction worker","male-construction-worker"],"u":"1f477-200d-2642-fe0f","v":["1f477-1f3fb-200d-2642-fe0f","1f477-1f3fc-200d-2642-fe0f","1f477-1f3fd-200d-2642-fe0f","1f477-1f3fe-200d-2642-fe0f","1f477-1f3ff-200d-2642-fe0f"]},{"n":["woman construction worker","female-construction-worker"],"u":"1f477-200d-2640-fe0f","v":["1f477-1f3fb-200d-2640-fe0f","1f477-1f3fc-200d-2640-fe0f","1f477-1f3fd-200d-2640-fe0f","1f477-1f3fe-200d-2640-fe0f","1f477-1f3ff-200d-2640-fe0f"]},{"n":["person with crown"],"u":"1fac5","v":["1fac5-1f3fb","1fac5-1f3fc","1fac5-1f3fd","1fac5-1f3fe","1fac5-1f3ff"]},{"n":["prince"],"u":"1f934","v":["1f934-1f3fb","1f934-1f3fc","1f934-1f3fd","1f934-1f3fe","1f934-1f3ff"]},{"n":["princess"],"u":"1f478","v":["1f478-1f3fb","1f478-1f3fc","1f478-1f3fd","1f478-1f3fe","1f478-1f3ff"]},{"n":["man with turban"],"u":"1f473","v":["1f473-1f3fb","1f473-1f3fc","1f473-1f3fd","1f473-1f3fe","1f473-1f3ff"]},{"n":["man wearing turban","man-wearing-turban"],"u":"1f473-200d-2642-fe0f","v":["1f473-1f3fb-200d-2642-fe0f","1f473-1f3fc-200d-2642-fe0f","1f473-1f3fd-200d-2642-fe0f","1f473-1f3fe-200d-2642-fe0f","1f473-1f3ff-200d-2642-fe0f"]},{"n":["woman wearing turban","woman-wearing-turban"],"u":"1f473-200d-2640-fe0f","v":["1f473-1f3fb-200d-2640-fe0f","1f473-1f3fc-200d-2640-fe0f","1f473-1f3fd-200d-2640-fe0f","1f473-1f3fe-200d-2640-fe0f","1f473-1f3ff-200d-2640-fe0f"]},{"n":["man with gua pi mao"],"u":"1f472","v":["1f472-1f3fb","1f472-1f3fc","1f472-1f3fd","1f472-1f3fe","1f472-1f3ff"]},{"n":["person with headscarf"],"u":"1f9d5","v":["1f9d5-1f3fb","1f9d5-1f3fc","1f9d5-1f3fd","1f9d5-1f3fe","1f9d5-1f3ff"]},{"n":["man in tuxedo","person in tuxedo"],"u":"1f935","v":["1f935-1f3fb","1f935-1f3fc","1f935-1f3fd","1f935-1f3fe","1f935-1f3ff"]},{"n":["man in tuxedo"],"u":"1f935-200d-2642-fe0f","v":["1f935-1f3fb-200d-2642-fe0f","1f935-1f3fc-200d-2642-fe0f","1f935-1f3fd-200d-2642-fe0f","1f935-1f3fe-200d-2642-fe0f","1f935-1f3ff-200d-2642-fe0f"]},{"n":["woman in tuxedo"],"u":"1f935-200d-2640-fe0f","v":["1f935-1f3fb-200d-2640-fe0f","1f935-1f3fc-200d-2640-fe0f","1f935-1f3fd-200d-2640-fe0f","1f935-1f3fe-200d-2640-fe0f","1f935-1f3ff-200d-2640-fe0f"]},{"n":["bride with veil"],"u":"1f470","v":["1f470-1f3fb","1f470-1f3fc","1f470-1f3fd","1f470-1f3fe","1f470-1f3ff"]},{"n":["man with veil"],"u":"1f470-200d-2642-fe0f","v":["1f470-1f3fb-200d-2642-fe0f","1f470-1f3fc-200d-2642-fe0f","1f470-1f3fd-200d-2642-fe0f","1f470-1f3fe-200d-2642-fe0f","1f470-1f3ff-200d-2642-fe0f"]},{"n":["woman with veil"],"u":"1f470-200d-2640-fe0f","v":["1f470-1f3fb-200d-2640-fe0f","1f470-1f3fc-200d-2640-fe0f","1f470-1f3fd-200d-2640-fe0f","1f470-1f3fe-200d-2640-fe0f","1f470-1f3ff-200d-2640-fe0f"]},{"n":["pregnant woman"],"u":"1f930","v":["1f930-1f3fb","1f930-1f3fc","1f930-1f3fd","1f930-1f3fe","1f930-1f3ff"]},{"n":["pregnant man"],"u":"1fac3","v":["1fac3-1f3fb","1fac3-1f3fc","1fac3-1f3fd","1fac3-1f3fe","1fac3-1f3ff"]},{"n":["pregnant person"],"u":"1fac4","v":["1fac4-1f3fb","1fac4-1f3fc","1fac4-1f3fd","1fac4-1f3fe","1fac4-1f3ff"]},{"n":["breast-feeding"],"u":"1f931","v":["1f931-1f3fb","1f931-1f3fc","1f931-1f3fd","1f931-1f3fe","1f931-1f3ff"]},{"n":["woman feeding baby"],"u":"1f469-200d-1f37c","v":["1f469-1f3fb-200d-1f37c","1f469-1f3fc-200d-1f37c","1f469-1f3fd-200d-1f37c","1f469-1f3fe-200d-1f37c","1f469-1f3ff-200d-1f37c"]},{"n":["man feeding baby"],"u":"1f468-200d-1f37c","v":["1f468-1f3fb-200d-1f37c","1f468-1f3fc-200d-1f37c","1f468-1f3fd-200d-1f37c","1f468-1f3fe-200d-1f37c","1f468-1f3ff-200d-1f37c"]},{"n":["person feeding baby"],"u":"1f9d1-200d-1f37c","v":["1f9d1-1f3fb-200d-1f37c","1f9d1-1f3fc-200d-1f37c","1f9d1-1f3fd-200d-1f37c","1f9d1-1f3fe-200d-1f37c","1f9d1-1f3ff-200d-1f37c"]},{"n":["angel","baby angel"],"u":"1f47c","v":["1f47c-1f3fb","1f47c-1f3fc","1f47c-1f3fd","1f47c-1f3fe","1f47c-1f3ff"]},{"n":["santa","father christmas"],"u":"1f385","v":["1f385-1f3fb","1f385-1f3fc","1f385-1f3fd","1f385-1f3fe","1f385-1f3ff"]},{"n":["mrs claus","mother christmas"],"u":"1f936","v":["1f936-1f3fb","1f936-1f3fc","1f936-1f3fd","1f936-1f3fe","1f936-1f3ff"]},{"n":["mx claus"],"u":"1f9d1-200d-1f384","v":["1f9d1-1f3fb-200d-1f384","1f9d1-1f3fc-200d-1f384","1f9d1-1f3fd-200d-1f384","1f9d1-1f3fe-200d-1f384","1f9d1-1f3ff-200d-1f384"]},{"n":["superhero"],"u":"1f9b8","v":["1f9b8-1f3fb","1f9b8-1f3fc","1f9b8-1f3fd","1f9b8-1f3fe","1f9b8-1f3ff"]},{"n":["man superhero","male superhero"],"u":"1f9b8-200d-2642-fe0f","v":["1f9b8-1f3fb-200d-2642-fe0f","1f9b8-1f3fc-200d-2642-fe0f","1f9b8-1f3fd-200d-2642-fe0f","1f9b8-1f3fe-200d-2642-fe0f","1f9b8-1f3ff-200d-2642-fe0f"]},{"n":["woman superhero","female superhero"],"u":"1f9b8-200d-2640-fe0f","v":["1f9b8-1f3fb-200d-2640-fe0f","1f9b8-1f3fc-200d-2640-fe0f","1f9b8-1f3fd-200d-2640-fe0f","1f9b8-1f3fe-200d-2640-fe0f","1f9b8-1f3ff-200d-2640-fe0f"]},{"n":["supervillain"],"u":"1f9b9","v":["1f9b9-1f3fb","1f9b9-1f3fc","1f9b9-1f3fd","1f9b9-1f3fe","1f9b9-1f3ff"]},{"n":["man supervillain","male supervillain"],"u":"1f9b9-200d-2642-fe0f","v":["1f9b9-1f3fb-200d-2642-fe0f","1f9b9-1f3fc-200d-2642-fe0f","1f9b9-1f3fd-200d-2642-fe0f","1f9b9-1f3fe-200d-2642-fe0f","1f9b9-1f3ff-200d-2642-fe0f"]},{"n":["woman supervillain","female supervillain"],"u":"1f9b9-200d-2640-fe0f","v":["1f9b9-1f3fb-200d-2640-fe0f","1f9b9-1f3fc-200d-2640-fe0f","1f9b9-1f3fd-200d-2640-fe0f","1f9b9-1f3fe-200d-2640-fe0f","1f9b9-1f3ff-200d-2640-fe0f"]},{"n":["mage"],"u":"1f9d9","v":["1f9d9-1f3fb","1f9d9-1f3fc","1f9d9-1f3fd","1f9d9-1f3fe","1f9d9-1f3ff"]},{"n":["man mage","male mage"],"u":"1f9d9-200d-2642-fe0f","v":["1f9d9-1f3fb-200d-2642-fe0f","1f9d9-1f3fc-200d-2642-fe0f","1f9d9-1f3fd-200d-2642-fe0f","1f9d9-1f3fe-200d-2642-fe0f","1f9d9-1f3ff-200d-2642-fe0f"]},{"n":["woman mage","female mage"],"u":"1f9d9-200d-2640-fe0f","v":["1f9d9-1f3fb-200d-2640-fe0f","1f9d9-1f3fc-200d-2640-fe0f","1f9d9-1f3fd-200d-2640-fe0f","1f9d9-1f3fe-200d-2640-fe0f","1f9d9-1f3ff-200d-2640-fe0f"]},{"n":["fairy"],"u":"1f9da","v":["1f9da-1f3fb","1f9da-1f3fc","1f9da-1f3fd","1f9da-1f3fe","1f9da-1f3ff"]},{"n":["man fairy","male fairy"],"u":"1f9da-200d-2642-fe0f","v":["1f9da-1f3fb-200d-2642-fe0f","1f9da-1f3fc-200d-2642-fe0f","1f9da-1f3fd-200d-2642-fe0f","1f9da-1f3fe-200d-2642-fe0f","1f9da-1f3ff-200d-2642-fe0f"]},{"n":["woman fairy","female fairy"],"u":"1f9da-200d-2640-fe0f","v":["1f9da-1f3fb-200d-2640-fe0f","1f9da-1f3fc-200d-2640-fe0f","1f9da-1f3fd-200d-2640-fe0f","1f9da-1f3fe-200d-2640-fe0f","1f9da-1f3ff-200d-2640-fe0f"]},{"n":["vampire"],"u":"1f9db","v":["1f9db-1f3fb","1f9db-1f3fc","1f9db-1f3fd","1f9db-1f3fe","1f9db-1f3ff"]},{"n":["man vampire","male vampire"],"u":"1f9db-200d-2642-fe0f","v":["1f9db-1f3fb-200d-2642-fe0f","1f9db-1f3fc-200d-2642-fe0f","1f9db-1f3fd-200d-2642-fe0f","1f9db-1f3fe-200d-2642-fe0f","1f9db-1f3ff-200d-2642-fe0f"]},{"n":["woman vampire","female vampire"],"u":"1f9db-200d-2640-fe0f","v":["1f9db-1f3fb-200d-2640-fe0f","1f9db-1f3fc-200d-2640-fe0f","1f9db-1f3fd-200d-2640-fe0f","1f9db-1f3fe-200d-2640-fe0f","1f9db-1f3ff-200d-2640-fe0f"]},{"n":["merperson"],"u":"1f9dc","v":["1f9dc-1f3fb","1f9dc-1f3fc","1f9dc-1f3fd","1f9dc-1f3fe","1f9dc-1f3ff"]},{"n":["merman"],"u":"1f9dc-200d-2642-fe0f","v":["1f9dc-1f3fb-200d-2642-fe0f","1f9dc-1f3fc-200d-2642-fe0f","1f9dc-1f3fd-200d-2642-fe0f","1f9dc-1f3fe-200d-2642-fe0f","1f9dc-1f3ff-200d-2642-fe0f"]},{"n":["mermaid"],"u":"1f9dc-200d-2640-fe0f","v":["1f9dc-1f3fb-200d-2640-fe0f","1f9dc-1f3fc-200d-2640-fe0f","1f9dc-1f3fd-200d-2640-fe0f","1f9dc-1f3fe-200d-2640-fe0f","1f9dc-1f3ff-200d-2640-fe0f"]},{"n":["elf"],"u":"1f9dd","v":["1f9dd-1f3fb","1f9dd-1f3fc","1f9dd-1f3fd","1f9dd-1f3fe","1f9dd-1f3ff"]},{"n":["man elf","male elf"],"u":"1f9dd-200d-2642-fe0f","v":["1f9dd-1f3fb-200d-2642-fe0f","1f9dd-1f3fc-200d-2642-fe0f","1f9dd-1f3fd-200d-2642-fe0f","1f9dd-1f3fe-200d-2642-fe0f","1f9dd-1f3ff-200d-2642-fe0f"]},{"n":["woman elf","female elf"],"u":"1f9dd-200d-2640-fe0f","v":["1f9dd-1f3fb-200d-2640-fe0f","1f9dd-1f3fc-200d-2640-fe0f","1f9dd-1f3fd-200d-2640-fe0f","1f9dd-1f3fe-200d-2640-fe0f","1f9dd-1f3ff-200d-2640-fe0f"]},{"n":["genie"],"u":"1f9de"},{"n":["man genie","male genie"],"u":"1f9de-200d-2642-fe0f"},{"n":["woman genie","female genie"],"u":"1f9de-200d-2640-fe0f"},{"n":["zombie"],"u":"1f9df"},{"n":["man zombie","male zombie"],"u":"1f9df-200d-2642-fe0f"},{"n":["woman zombie","female zombie"],"u":"1f9df-200d-2640-fe0f"},{"n":["troll"],"u":"1f9cc"},{"n":["massage","face massage"],"u":"1f486","v":["1f486-1f3fb","1f486-1f3fc","1f486-1f3fd","1f486-1f3fe","1f486-1f3ff"]},{"n":["man getting massage","man-getting-massage"],"u":"1f486-200d-2642-fe0f","v":["1f486-1f3fb-200d-2642-fe0f","1f486-1f3fc-200d-2642-fe0f","1f486-1f3fd-200d-2642-fe0f","1f486-1f3fe-200d-2642-fe0f","1f486-1f3ff-200d-2642-fe0f"]},{"n":["woman getting massage","woman-getting-massage"],"u":"1f486-200d-2640-fe0f","v":["1f486-1f3fb-200d-2640-fe0f","1f486-1f3fc-200d-2640-fe0f","1f486-1f3fd-200d-2640-fe0f","1f486-1f3fe-200d-2640-fe0f","1f486-1f3ff-200d-2640-fe0f"]},{"n":["haircut"],"u":"1f487","v":["1f487-1f3fb","1f487-1f3fc","1f487-1f3fd","1f487-1f3fe","1f487-1f3ff"]},{"n":["man getting haircut","man-getting-haircut"],"u":"1f487-200d-2642-fe0f","v":["1f487-1f3fb-200d-2642-fe0f","1f487-1f3fc-200d-2642-fe0f","1f487-1f3fd-200d-2642-fe0f","1f487-1f3fe-200d-2642-fe0f","1f487-1f3ff-200d-2642-fe0f"]},{"n":["woman getting haircut","woman-getting-haircut"],"u":"1f487-200d-2640-fe0f","v":["1f487-1f3fb-200d-2640-fe0f","1f487-1f3fc-200d-2640-fe0f","1f487-1f3fd-200d-2640-fe0f","1f487-1f3fe-200d-2640-fe0f","1f487-1f3ff-200d-2640-fe0f"]},{"n":["walking","pedestrian"],"u":"1f6b6","v":["1f6b6-1f3fb","1f6b6-1f3fc","1f6b6-1f3fd","1f6b6-1f3fe","1f6b6-1f3ff"]},{"n":["man walking","man-walking"],"u":"1f6b6-200d-2642-fe0f","v":["1f6b6-1f3fb-200d-2642-fe0f","1f6b6-1f3fc-200d-2642-fe0f","1f6b6-1f3fd-200d-2642-fe0f","1f6b6-1f3fe-200d-2642-fe0f","1f6b6-1f3ff-200d-2642-fe0f"]},{"n":["woman walking","woman-walking"],"u":"1f6b6-200d-2640-fe0f","v":["1f6b6-1f3fb-200d-2640-fe0f","1f6b6-1f3fc-200d-2640-fe0f","1f6b6-1f3fd-200d-2640-fe0f","1f6b6-1f3fe-200d-2640-fe0f","1f6b6-1f3ff-200d-2640-fe0f"]},{"n":["standing person"],"u":"1f9cd","v":["1f9cd-1f3fb","1f9cd-1f3fc","1f9cd-1f3fd","1f9cd-1f3fe","1f9cd-1f3ff"]},{"n":["man standing"],"u":"1f9cd-200d-2642-fe0f","v":["1f9cd-1f3fb-200d-2642-fe0f","1f9cd-1f3fc-200d-2642-fe0f","1f9cd-1f3fd-200d-2642-fe0f","1f9cd-1f3fe-200d-2642-fe0f","1f9cd-1f3ff-200d-2642-fe0f"]},{"n":["woman standing"],"u":"1f9cd-200d-2640-fe0f","v":["1f9cd-1f3fb-200d-2640-fe0f","1f9cd-1f3fc-200d-2640-fe0f","1f9cd-1f3fd-200d-2640-fe0f","1f9cd-1f3fe-200d-2640-fe0f","1f9cd-1f3ff-200d-2640-fe0f"]},{"n":["kneeling person"],"u":"1f9ce","v":["1f9ce-1f3fb","1f9ce-1f3fc","1f9ce-1f3fd","1f9ce-1f3fe","1f9ce-1f3ff"]},{"n":["man kneeling"],"u":"1f9ce-200d-2642-fe0f","v":["1f9ce-1f3fb-200d-2642-fe0f","1f9ce-1f3fc-200d-2642-fe0f","1f9ce-1f3fd-200d-2642-fe0f","1f9ce-1f3fe-200d-2642-fe0f","1f9ce-1f3ff-200d-2642-fe0f"]},{"n":["woman kneeling"],"u":"1f9ce-200d-2640-fe0f","v":["1f9ce-1f3fb-200d-2640-fe0f","1f9ce-1f3fc-200d-2640-fe0f","1f9ce-1f3fd-200d-2640-fe0f","1f9ce-1f3fe-200d-2640-fe0f","1f9ce-1f3ff-200d-2640-fe0f"]},{"n":["person with white cane","person with probing cane"],"u":"1f9d1-200d-1f9af","v":["1f9d1-1f3fb-200d-1f9af","1f9d1-1f3fc-200d-1f9af","1f9d1-1f3fd-200d-1f9af","1f9d1-1f3fe-200d-1f9af","1f9d1-1f3ff-200d-1f9af"]},{"n":["man with white cane","man with probing cane"],"u":"1f468-200d-1f9af","v":["1f468-1f3fb-200d-1f9af","1f468-1f3fc-200d-1f9af","1f468-1f3fd-200d-1f9af","1f468-1f3fe-200d-1f9af","1f468-1f3ff-200d-1f9af"]},{"n":["woman with white cane","woman with probing cane"],"u":"1f469-200d-1f9af","v":["1f469-1f3fb-200d-1f9af","1f469-1f3fc-200d-1f9af","1f469-1f3fd-200d-1f9af","1f469-1f3fe-200d-1f9af","1f469-1f3ff-200d-1f9af"]},{"n":["person in motorized wheelchair"],"u":"1f9d1-200d-1f9bc","v":["1f9d1-1f3fb-200d-1f9bc","1f9d1-1f3fc-200d-1f9bc","1f9d1-1f3fd-200d-1f9bc","1f9d1-1f3fe-200d-1f9bc","1f9d1-1f3ff-200d-1f9bc"]},{"n":["man in motorized wheelchair"],"u":"1f468-200d-1f9bc","v":["1f468-1f3fb-200d-1f9bc","1f468-1f3fc-200d-1f9bc","1f468-1f3fd-200d-1f9bc","1f468-1f3fe-200d-1f9bc","1f468-1f3ff-200d-1f9bc"]},{"n":["woman in motorized wheelchair"],"u":"1f469-200d-1f9bc","v":["1f469-1f3fb-200d-1f9bc","1f469-1f3fc-200d-1f9bc","1f469-1f3fd-200d-1f9bc","1f469-1f3fe-200d-1f9bc","1f469-1f3ff-200d-1f9bc"]},{"n":["person in manual wheelchair"],"u":"1f9d1-200d-1f9bd","v":["1f9d1-1f3fb-200d-1f9bd","1f9d1-1f3fc-200d-1f9bd","1f9d1-1f3fd-200d-1f9bd","1f9d1-1f3fe-200d-1f9bd","1f9d1-1f3ff-200d-1f9bd"]},{"n":["man in manual wheelchair"],"u":"1f468-200d-1f9bd","v":["1f468-1f3fb-200d-1f9bd","1f468-1f3fc-200d-1f9bd","1f468-1f3fd-200d-1f9bd","1f468-1f3fe-200d-1f9bd","1f468-1f3ff-200d-1f9bd"]},{"n":["woman in manual wheelchair"],"u":"1f469-200d-1f9bd","v":["1f469-1f3fb-200d-1f9bd","1f469-1f3fc-200d-1f9bd","1f469-1f3fd-200d-1f9bd","1f469-1f3fe-200d-1f9bd","1f469-1f3ff-200d-1f9bd"]},{"n":["runner","running"],"u":"1f3c3","v":["1f3c3-1f3fb","1f3c3-1f3fc","1f3c3-1f3fd","1f3c3-1f3fe","1f3c3-1f3ff"]},{"n":["man running","man-running"],"u":"1f3c3-200d-2642-fe0f","v":["1f3c3-1f3fb-200d-2642-fe0f","1f3c3-1f3fc-200d-2642-fe0f","1f3c3-1f3fd-200d-2642-fe0f","1f3c3-1f3fe-200d-2642-fe0f","1f3c3-1f3ff-200d-2642-fe0f"]},{"n":["woman running","woman-running"],"u":"1f3c3-200d-2640-fe0f","v":["1f3c3-1f3fb-200d-2640-fe0f","1f3c3-1f3fc-200d-2640-fe0f","1f3c3-1f3fd-200d-2640-fe0f","1f3c3-1f3fe-200d-2640-fe0f","1f3c3-1f3ff-200d-2640-fe0f"]},{"n":["dancer"],"u":"1f483","v":["1f483-1f3fb","1f483-1f3fc","1f483-1f3fd","1f483-1f3fe","1f483-1f3ff"]},{"n":["man dancing"],"u":"1f57a","v":["1f57a-1f3fb","1f57a-1f3fc","1f57a-1f3fd","1f57a-1f3fe","1f57a-1f3ff"]},{"n":["person in suit levitating","man in business suit levitating"],"u":"1f574-fe0f","v":["1f574-1f3fb","1f574-1f3fc","1f574-1f3fd","1f574-1f3fe","1f574-1f3ff"]},{"n":["dancers","woman with bunny ears"],"u":"1f46f"},{"n":["men with bunny ears","men-with-bunny-ears-partying","man-with-bunny-ears-partying"],"u":"1f46f-200d-2642-fe0f"},{"n":["women with bunny ears","women-with-bunny-ears-partying","woman-with-bunny-ears-partying"],"u":"1f46f-200d-2640-fe0f"},{"n":["person in steamy room"],"u":"1f9d6","v":["1f9d6-1f3fb","1f9d6-1f3fc","1f9d6-1f3fd","1f9d6-1f3fe","1f9d6-1f3ff"]},{"n":["man in steamy room"],"u":"1f9d6-200d-2642-fe0f","v":["1f9d6-1f3fb-200d-2642-fe0f","1f9d6-1f3fc-200d-2642-fe0f","1f9d6-1f3fd-200d-2642-fe0f","1f9d6-1f3fe-200d-2642-fe0f","1f9d6-1f3ff-200d-2642-fe0f"]},{"n":["woman in steamy room"],"u":"1f9d6-200d-2640-fe0f","v":["1f9d6-1f3fb-200d-2640-fe0f","1f9d6-1f3fc-200d-2640-fe0f","1f9d6-1f3fd-200d-2640-fe0f","1f9d6-1f3fe-200d-2640-fe0f","1f9d6-1f3ff-200d-2640-fe0f"]},{"n":["person climbing"],"u":"1f9d7","v":["1f9d7-1f3fb","1f9d7-1f3fc","1f9d7-1f3fd","1f9d7-1f3fe","1f9d7-1f3ff"]},{"n":["man climbing"],"u":"1f9d7-200d-2642-fe0f","v":["1f9d7-1f3fb-200d-2642-fe0f","1f9d7-1f3fc-200d-2642-fe0f","1f9d7-1f3fd-200d-2642-fe0f","1f9d7-1f3fe-200d-2642-fe0f","1f9d7-1f3ff-200d-2642-fe0f"]},{"n":["woman climbing"],"u":"1f9d7-200d-2640-fe0f","v":["1f9d7-1f3fb-200d-2640-fe0f","1f9d7-1f3fc-200d-2640-fe0f","1f9d7-1f3fd-200d-2640-fe0f","1f9d7-1f3fe-200d-2640-fe0f","1f9d7-1f3ff-200d-2640-fe0f"]},{"n":["fencer"],"u":"1f93a"},{"n":["horse racing"],"u":"1f3c7","v":["1f3c7-1f3fb","1f3c7-1f3fc","1f3c7-1f3fd","1f3c7-1f3fe","1f3c7-1f3ff"]},{"n":["skier"],"u":"26f7-fe0f"},{"n":["snowboarder"],"u":"1f3c2","v":["1f3c2-1f3fb","1f3c2-1f3fc","1f3c2-1f3fd","1f3c2-1f3fe","1f3c2-1f3ff"]},{"n":["golfer","person golfing"],"u":"1f3cc-fe0f","v":["1f3cc-1f3fb","1f3cc-1f3fc","1f3cc-1f3fd","1f3cc-1f3fe","1f3cc-1f3ff"]},{"n":["man golfing","man-golfing"],"u":"1f3cc-fe0f-200d-2642-fe0f","v":["1f3cc-1f3fb-200d-2642-fe0f","1f3cc-1f3fc-200d-2642-fe0f","1f3cc-1f3fd-200d-2642-fe0f","1f3cc-1f3fe-200d-2642-fe0f","1f3cc-1f3ff-200d-2642-fe0f"]},{"n":["woman golfing","woman-golfing"],"u":"1f3cc-fe0f-200d-2640-fe0f","v":["1f3cc-1f3fb-200d-2640-fe0f","1f3cc-1f3fc-200d-2640-fe0f","1f3cc-1f3fd-200d-2640-fe0f","1f3cc-1f3fe-200d-2640-fe0f","1f3cc-1f3ff-200d-2640-fe0f"]},{"n":["surfer"],"u":"1f3c4","v":["1f3c4-1f3fb","1f3c4-1f3fc","1f3c4-1f3fd","1f3c4-1f3fe","1f3c4-1f3ff"]},{"n":["man surfing","man-surfing"],"u":"1f3c4-200d-2642-fe0f","v":["1f3c4-1f3fb-200d-2642-fe0f","1f3c4-1f3fc-200d-2642-fe0f","1f3c4-1f3fd-200d-2642-fe0f","1f3c4-1f3fe-200d-2642-fe0f","1f3c4-1f3ff-200d-2642-fe0f"]},{"n":["woman surfing","woman-surfing"],"u":"1f3c4-200d-2640-fe0f","v":["1f3c4-1f3fb-200d-2640-fe0f","1f3c4-1f3fc-200d-2640-fe0f","1f3c4-1f3fd-200d-2640-fe0f","1f3c4-1f3fe-200d-2640-fe0f","1f3c4-1f3ff-200d-2640-fe0f"]},{"n":["rowboat"],"u":"1f6a3","v":["1f6a3-1f3fb","1f6a3-1f3fc","1f6a3-1f3fd","1f6a3-1f3fe","1f6a3-1f3ff"]},{"n":["man rowing boat","man-rowing-boat"],"u":"1f6a3-200d-2642-fe0f","v":["1f6a3-1f3fb-200d-2642-fe0f","1f6a3-1f3fc-200d-2642-fe0f","1f6a3-1f3fd-200d-2642-fe0f","1f6a3-1f3fe-200d-2642-fe0f","1f6a3-1f3ff-200d-2642-fe0f"]},{"n":["woman rowing boat","woman-rowing-boat"],"u":"1f6a3-200d-2640-fe0f","v":["1f6a3-1f3fb-200d-2640-fe0f","1f6a3-1f3fc-200d-2640-fe0f","1f6a3-1f3fd-200d-2640-fe0f","1f6a3-1f3fe-200d-2640-fe0f","1f6a3-1f3ff-200d-2640-fe0f"]},{"n":["swimmer"],"u":"1f3ca","v":["1f3ca-1f3fb","1f3ca-1f3fc","1f3ca-1f3fd","1f3ca-1f3fe","1f3ca-1f3ff"]},{"n":["man swimming","man-swimming"],"u":"1f3ca-200d-2642-fe0f","v":["1f3ca-1f3fb-200d-2642-fe0f","1f3ca-1f3fc-200d-2642-fe0f","1f3ca-1f3fd-200d-2642-fe0f","1f3ca-1f3fe-200d-2642-fe0f","1f3ca-1f3ff-200d-2642-fe0f"]},{"n":["woman swimming","woman-swimming"],"u":"1f3ca-200d-2640-fe0f","v":["1f3ca-1f3fb-200d-2640-fe0f","1f3ca-1f3fc-200d-2640-fe0f","1f3ca-1f3fd-200d-2640-fe0f","1f3ca-1f3fe-200d-2640-fe0f","1f3ca-1f3ff-200d-2640-fe0f"]},{"n":["person with ball","person bouncing ball"],"u":"26f9-fe0f","v":["26f9-1f3fb","26f9-1f3fc","26f9-1f3fd","26f9-1f3fe","26f9-1f3ff"]},{"n":["man bouncing ball","man-bouncing-ball"],"u":"26f9-fe0f-200d-2642-fe0f","v":["26f9-1f3fb-200d-2642-fe0f","26f9-1f3fc-200d-2642-fe0f","26f9-1f3fd-200d-2642-fe0f","26f9-1f3fe-200d-2642-fe0f","26f9-1f3ff-200d-2642-fe0f"]},{"n":["woman bouncing ball","woman-bouncing-ball"],"u":"26f9-fe0f-200d-2640-fe0f","v":["26f9-1f3fb-200d-2640-fe0f","26f9-1f3fc-200d-2640-fe0f","26f9-1f3fd-200d-2640-fe0f","26f9-1f3fe-200d-2640-fe0f","26f9-1f3ff-200d-2640-fe0f"]},{"n":["weight lifter","person lifting weights"],"u":"1f3cb-fe0f","v":["1f3cb-1f3fb","1f3cb-1f3fc","1f3cb-1f3fd","1f3cb-1f3fe","1f3cb-1f3ff"]},{"n":["man lifting weights","man-lifting-weights"],"u":"1f3cb-fe0f-200d-2642-fe0f","v":["1f3cb-1f3fb-200d-2642-fe0f","1f3cb-1f3fc-200d-2642-fe0f","1f3cb-1f3fd-200d-2642-fe0f","1f3cb-1f3fe-200d-2642-fe0f","1f3cb-1f3ff-200d-2642-fe0f"]},{"n":["woman lifting weights","woman-lifting-weights"],"u":"1f3cb-fe0f-200d-2640-fe0f","v":["1f3cb-1f3fb-200d-2640-fe0f","1f3cb-1f3fc-200d-2640-fe0f","1f3cb-1f3fd-200d-2640-fe0f","1f3cb-1f3fe-200d-2640-fe0f","1f3cb-1f3ff-200d-2640-fe0f"]},{"n":["bicyclist"],"u":"1f6b4","v":["1f6b4-1f3fb","1f6b4-1f3fc","1f6b4-1f3fd","1f6b4-1f3fe","1f6b4-1f3ff"]},{"n":["man biking","man-biking"],"u":"1f6b4-200d-2642-fe0f","v":["1f6b4-1f3fb-200d-2642-fe0f","1f6b4-1f3fc-200d-2642-fe0f","1f6b4-1f3fd-200d-2642-fe0f","1f6b4-1f3fe-200d-2642-fe0f","1f6b4-1f3ff-200d-2642-fe0f"]},{"n":["woman biking","woman-biking"],"u":"1f6b4-200d-2640-fe0f","v":["1f6b4-1f3fb-200d-2640-fe0f","1f6b4-1f3fc-200d-2640-fe0f","1f6b4-1f3fd-200d-2640-fe0f","1f6b4-1f3fe-200d-2640-fe0f","1f6b4-1f3ff-200d-2640-fe0f"]},{"n":["mountain bicyclist"],"u":"1f6b5","v":["1f6b5-1f3fb","1f6b5-1f3fc","1f6b5-1f3fd","1f6b5-1f3fe","1f6b5-1f3ff"]},{"n":["man mountain biking","man-mountain-biking"],"u":"1f6b5-200d-2642-fe0f","v":["1f6b5-1f3fb-200d-2642-fe0f","1f6b5-1f3fc-200d-2642-fe0f","1f6b5-1f3fd-200d-2642-fe0f","1f6b5-1f3fe-200d-2642-fe0f","1f6b5-1f3ff-200d-2642-fe0f"]},{"n":["woman mountain biking","woman-mountain-biking"],"u":"1f6b5-200d-2640-fe0f","v":["1f6b5-1f3fb-200d-2640-fe0f","1f6b5-1f3fc-200d-2640-fe0f","1f6b5-1f3fd-200d-2640-fe0f","1f6b5-1f3fe-200d-2640-fe0f","1f6b5-1f3ff-200d-2640-fe0f"]},{"n":["person doing cartwheel"],"u":"1f938","v":["1f938-1f3fb","1f938-1f3fc","1f938-1f3fd","1f938-1f3fe","1f938-1f3ff"]},{"n":["man cartwheeling","man-cartwheeling"],"u":"1f938-200d-2642-fe0f","v":["1f938-1f3fb-200d-2642-fe0f","1f938-1f3fc-200d-2642-fe0f","1f938-1f3fd-200d-2642-fe0f","1f938-1f3fe-200d-2642-fe0f","1f938-1f3ff-200d-2642-fe0f"]},{"n":["woman cartwheeling","woman-cartwheeling"],"u":"1f938-200d-2640-fe0f","v":["1f938-1f3fb-200d-2640-fe0f","1f938-1f3fc-200d-2640-fe0f","1f938-1f3fd-200d-2640-fe0f","1f938-1f3fe-200d-2640-fe0f","1f938-1f3ff-200d-2640-fe0f"]},{"n":["wrestlers"],"u":"1f93c"},{"n":["men wrestling","man-wrestling"],"u":"1f93c-200d-2642-fe0f"},{"n":["women wrestling","woman-wrestling"],"u":"1f93c-200d-2640-fe0f"},{"n":["water polo"],"u":"1f93d","v":["1f93d-1f3fb","1f93d-1f3fc","1f93d-1f3fd","1f93d-1f3fe","1f93d-1f3ff"]},{"n":["man playing water polo","man-playing-water-polo"],"u":"1f93d-200d-2642-fe0f","v":["1f93d-1f3fb-200d-2642-fe0f","1f93d-1f3fc-200d-2642-fe0f","1f93d-1f3fd-200d-2642-fe0f","1f93d-1f3fe-200d-2642-fe0f","1f93d-1f3ff-200d-2642-fe0f"]},{"n":["woman playing water polo","woman-playing-water-polo"],"u":"1f93d-200d-2640-fe0f","v":["1f93d-1f3fb-200d-2640-fe0f","1f93d-1f3fc-200d-2640-fe0f","1f93d-1f3fd-200d-2640-fe0f","1f93d-1f3fe-200d-2640-fe0f","1f93d-1f3ff-200d-2640-fe0f"]},{"n":["handball"],"u":"1f93e","v":["1f93e-1f3fb","1f93e-1f3fc","1f93e-1f3fd","1f93e-1f3fe","1f93e-1f3ff"]},{"n":["man playing handball","man-playing-handball"],"u":"1f93e-200d-2642-fe0f","v":["1f93e-1f3fb-200d-2642-fe0f","1f93e-1f3fc-200d-2642-fe0f","1f93e-1f3fd-200d-2642-fe0f","1f93e-1f3fe-200d-2642-fe0f","1f93e-1f3ff-200d-2642-fe0f"]},{"n":["woman playing handball","woman-playing-handball"],"u":"1f93e-200d-2640-fe0f","v":["1f93e-1f3fb-200d-2640-fe0f","1f93e-1f3fc-200d-2640-fe0f","1f93e-1f3fd-200d-2640-fe0f","1f93e-1f3fe-200d-2640-fe0f","1f93e-1f3ff-200d-2640-fe0f"]},{"n":["juggling"],"u":"1f939","v":["1f939-1f3fb","1f939-1f3fc","1f939-1f3fd","1f939-1f3fe","1f939-1f3ff"]},{"n":["man juggling","man-juggling"],"u":"1f939-200d-2642-fe0f","v":["1f939-1f3fb-200d-2642-fe0f","1f939-1f3fc-200d-2642-fe0f","1f939-1f3fd-200d-2642-fe0f","1f939-1f3fe-200d-2642-fe0f","1f939-1f3ff-200d-2642-fe0f"]},{"n":["woman juggling","woman-juggling"],"u":"1f939-200d-2640-fe0f","v":["1f939-1f3fb-200d-2640-fe0f","1f939-1f3fc-200d-2640-fe0f","1f939-1f3fd-200d-2640-fe0f","1f939-1f3fe-200d-2640-fe0f","1f939-1f3ff-200d-2640-fe0f"]},{"n":["person in lotus position"],"u":"1f9d8","v":["1f9d8-1f3fb","1f9d8-1f3fc","1f9d8-1f3fd","1f9d8-1f3fe","1f9d8-1f3ff"]},{"n":["man in lotus position"],"u":"1f9d8-200d-2642-fe0f","v":["1f9d8-1f3fb-200d-2642-fe0f","1f9d8-1f3fc-200d-2642-fe0f","1f9d8-1f3fd-200d-2642-fe0f","1f9d8-1f3fe-200d-2642-fe0f","1f9d8-1f3ff-200d-2642-fe0f"]},{"n":["woman in lotus position"],"u":"1f9d8-200d-2640-fe0f","v":["1f9d8-1f3fb-200d-2640-fe0f","1f9d8-1f3fc-200d-2640-fe0f","1f9d8-1f3fd-200d-2640-fe0f","1f9d8-1f3fe-200d-2640-fe0f","1f9d8-1f3ff-200d-2640-fe0f"]},{"n":["bath"],"u":"1f6c0","v":["1f6c0-1f3fb","1f6c0-1f3fc","1f6c0-1f3fd","1f6c0-1f3fe","1f6c0-1f3ff"]},{"n":["sleeping accommodation"],"u":"1f6cc","v":["1f6cc-1f3fb","1f6cc-1f3fc","1f6cc-1f3fd","1f6cc-1f3fe","1f6cc-1f3ff"]},{"n":["people holding hands"],"u":"1f9d1-200d-1f91d-200d-1f9d1","v":["1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb","1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc","1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd","1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe","1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff","1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb","1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc","1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd","1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe","1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff","1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb","1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc","1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd","1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe","1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff","1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb","1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc","1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd","1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe","1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff","1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb","1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc","1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd","1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe","1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff"]},{"n":["women holding hands","two women holding hands"],"u":"1f46d","v":["1f46d-1f3fb","1f46d-1f3fc","1f46d-1f3fd","1f46d-1f3fe","1f46d-1f3ff","1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc","1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd","1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe","1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff","1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb","1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd","1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe","1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff","1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb","1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc","1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe","1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff","1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb","1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc","1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd","1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff","1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb","1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc","1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd","1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe"]},{"n":["couple","man and woman holding hands","woman and man holding hands"],"u":"1f46b","v":["1f46b-1f3fb","1f46b-1f3fc","1f46b-1f3fd","1f46b-1f3fe","1f46b-1f3ff","1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc","1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd","1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe","1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff","1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb","1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd","1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe","1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff","1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb","1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc","1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe","1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff","1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb","1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc","1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd","1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff","1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb","1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc","1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd","1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe"]},{"n":["men holding hands","two men holding hands"],"u":"1f46c","v":["1f46c-1f3fb","1f46c-1f3fc","1f46c-1f3fd","1f46c-1f3fe","1f46c-1f3ff","1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc","1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd","1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe","1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff","1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb","1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd","1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe","1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff","1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb","1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc","1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe","1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff","1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb","1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc","1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd","1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff","1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb","1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc","1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd","1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe"]},{"n":["kiss","couplekiss"],"u":"1f48f","v":["1f48f-1f3fb","1f48f-1f3fc","1f48f-1f3fd","1f48f-1f3fe","1f48f-1f3ff","1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe","1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff","1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb","1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc","1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd","1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe"]},{"n":["woman-kiss-man","kiss: woman, man"],"u":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","v":["1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff"]},{"n":["man-kiss-man","kiss: man, man"],"u":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","v":["1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff"]},{"n":["woman-kiss-woman","kiss: woman, woman"],"u":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","v":["1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff"]},{"n":["couple with heart"],"u":"1f491","v":["1f491-1f3fb","1f491-1f3fc","1f491-1f3fd","1f491-1f3fe","1f491-1f3ff","1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc","1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd","1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe","1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff","1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb","1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd","1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe","1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff","1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb","1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc","1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe","1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff","1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb","1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc","1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd","1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff","1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb","1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc","1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd","1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe"]},{"n":["woman-heart-man","couple with heart: woman, man"],"u":"1f469-200d-2764-fe0f-200d-1f468","v":["1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc","1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd","1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe","1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff","1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb","1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd","1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe","1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff","1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb","1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc","1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe","1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff","1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb","1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc","1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd","1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff","1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb","1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc","1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd","1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe","1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff"]},{"n":["man-heart-man","couple with heart: man, man"],"u":"1f468-200d-2764-fe0f-200d-1f468","v":["1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc","1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd","1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe","1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff","1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb","1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd","1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe","1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff","1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb","1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc","1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe","1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff","1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb","1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc","1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd","1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff","1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb","1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc","1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd","1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe","1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff"]},{"n":["woman-heart-woman","couple with heart: woman, woman"],"u":"1f469-200d-2764-fe0f-200d-1f469","v":["1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb","1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc","1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd","1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe","1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff","1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb","1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc","1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd","1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe","1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff","1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb","1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc","1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd","1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe","1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff","1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb","1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc","1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd","1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe","1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff","1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb","1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc","1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd","1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe","1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff"]},{"n":["family"],"u":"1f46a"},{"n":["man-woman-boy","family: man, woman, boy"],"u":"1f468-200d-1f469-200d-1f466"},{"n":["man-woman-girl","family: man, woman, girl"],"u":"1f468-200d-1f469-200d-1f467"},{"n":["man-woman-girl-boy","family: man, woman, girl, boy"],"u":"1f468-200d-1f469-200d-1f467-200d-1f466"},{"n":["man-woman-boy-boy","family: man, woman, boy, boy"],"u":"1f468-200d-1f469-200d-1f466-200d-1f466"},{"n":["man-woman-girl-girl","family: man, woman, girl, girl"],"u":"1f468-200d-1f469-200d-1f467-200d-1f467"},{"n":["man-man-boy","family: man, man, boy"],"u":"1f468-200d-1f468-200d-1f466"},{"n":["man-man-girl","family: man, man, girl"],"u":"1f468-200d-1f468-200d-1f467"},{"n":["man-man-girl-boy","family: man, man, girl, boy"],"u":"1f468-200d-1f468-200d-1f467-200d-1f466"},{"n":["man-man-boy-boy","family: man, man, boy, boy"],"u":"1f468-200d-1f468-200d-1f466-200d-1f466"},{"n":["man-man-girl-girl","family: man, man, girl, girl"],"u":"1f468-200d-1f468-200d-1f467-200d-1f467"},{"n":["woman-woman-boy","family: woman, woman, boy"],"u":"1f469-200d-1f469-200d-1f466"},{"n":["woman-woman-girl","family: woman, woman, girl"],"u":"1f469-200d-1f469-200d-1f467"},{"n":["woman-woman-girl-boy","family: woman, woman, girl, boy"],"u":"1f469-200d-1f469-200d-1f467-200d-1f466"},{"n":["woman-woman-boy-boy","family: woman, woman, boy, boy"],"u":"1f469-200d-1f469-200d-1f466-200d-1f466"},{"n":["woman-woman-girl-girl","family: woman, woman, girl, girl"],"u":"1f469-200d-1f469-200d-1f467-200d-1f467"},{"n":["man-boy","family: man, boy"],"u":"1f468-200d-1f466"},{"n":["man-boy-boy","family: man, boy, boy"],"u":"1f468-200d-1f466-200d-1f466"},{"n":["man-girl","family: man, girl"],"u":"1f468-200d-1f467"},{"n":["man-girl-boy","family: man, girl, boy"],"u":"1f468-200d-1f467-200d-1f466"},{"n":["man-girl-girl","family: man, girl, girl"],"u":"1f468-200d-1f467-200d-1f467"},{"n":["woman-boy","family: woman, boy"],"u":"1f469-200d-1f466"},{"n":["woman-boy-boy","family: woman, boy, boy"],"u":"1f469-200d-1f466-200d-1f466"},{"n":["woman-girl","family: woman, girl"],"u":"1f469-200d-1f467"},{"n":["woman-girl-boy","family: woman, girl, boy"],"u":"1f469-200d-1f467-200d-1f466"},{"n":["woman-girl-girl","family: woman, girl, girl"],"u":"1f469-200d-1f467-200d-1f467"},{"n":["speaking head","speaking head in silhouette"],"u":"1f5e3-fe0f"},{"n":["bust in silhouette"],"u":"1f464"},{"n":["busts in silhouette"],"u":"1f465"},{"n":["people hugging"],"u":"1fac2"},{"n":["footprints"],"u":"1f463"}],"animals_nature":[{"n":["monkey face"],"u":"1f435"},{"n":["monkey"],"u":"1f412"},{"n":["gorilla"],"u":"1f98d"},{"n":["orangutan"],"u":"1f9a7"},{"n":["dog","dog face"],"u":"1f436"},{"n":["dog","dog2"],"u":"1f415"},{"n":["guide dog"],"u":"1f9ae"},{"n":["service dog"],"u":"1f415-200d-1f9ba"},{"n":["poodle"],"u":"1f429"},{"n":["wolf","wolf face"],"u":"1f43a"},{"n":["fox face"],"u":"1f98a"},{"n":["raccoon"],"u":"1f99d"},{"n":["cat","cat face"],"u":"1f431"},{"n":["cat","cat2"],"u":"1f408"},{"n":["black cat"],"u":"1f408-200d-2b1b"},{"n":["lion face"],"u":"1f981"},{"n":["tiger","tiger face"],"u":"1f42f"},{"n":["tiger","tiger2"],"u":"1f405"},{"n":["leopard"],"u":"1f406"},{"n":["horse","horse face"],"u":"1f434"},{"n":["horse","racehorse"],"u":"1f40e"},{"n":["unicorn face"],"u":"1f984"},{"n":["zebra face"],"u":"1f993"},{"n":["deer"],"u":"1f98c"},{"n":["bison"],"u":"1f9ac"},{"n":["cow","cow face"],"u":"1f42e"},{"n":["ox"],"u":"1f402"},{"n":["water buffalo"],"u":"1f403"},{"n":["cow","cow2"],"u":"1f404"},{"n":["pig","pig face"],"u":"1f437"},{"n":["pig","pig2"],"u":"1f416"},{"n":["boar"],"u":"1f417"},{"n":["pig nose"],"u":"1f43d"},{"n":["ram"],"u":"1f40f"},{"n":["sheep"],"u":"1f411"},{"n":["goat"],"u":"1f410"},{"n":["dromedary camel"],"u":"1f42a"},{"n":["camel","bactrian camel"],"u":"1f42b"},{"n":["llama"],"u":"1f999"},{"n":["giraffe face"],"u":"1f992"},{"n":["elephant"],"u":"1f418"},{"n":["mammoth"],"u":"1f9a3"},{"n":["rhinoceros"],"u":"1f98f"},{"n":["hippopotamus"],"u":"1f99b"},{"n":["mouse","mouse face"],"u":"1f42d"},{"n":["mouse","mouse2"],"u":"1f401"},{"n":["rat"],"u":"1f400"},{"n":["hamster","hamster face"],"u":"1f439"},{"n":["rabbit","rabbit face"],"u":"1f430"},{"n":["rabbit","rabbit2"],"u":"1f407"},{"n":["chipmunk"],"u":"1f43f-fe0f"},{"n":["beaver"],"u":"1f9ab"},{"n":["hedgehog"],"u":"1f994"},{"n":["bat"],"u":"1f987"},{"n":["bear","bear face"],"u":"1f43b"},{"n":["polar bear"],"u":"1f43b-200d-2744-fe0f"},{"n":["koala"],"u":"1f428"},{"n":["panda face"],"u":"1f43c"},{"n":["sloth"],"u":"1f9a5"},{"n":["otter"],"u":"1f9a6"},{"n":["skunk"],"u":"1f9a8"},{"n":["kangaroo"],"u":"1f998"},{"n":["badger"],"u":"1f9a1"},{"n":["feet","paw prints"],"u":"1f43e"},{"n":["turkey"],"u":"1f983"},{"n":["chicken"],"u":"1f414"},{"n":["rooster"],"u":"1f413"},{"n":["hatching chick"],"u":"1f423"},{"n":["baby chick"],"u":"1f424"},{"n":["hatched chick","front-facing baby chick"],"u":"1f425"},{"n":["bird"],"u":"1f426"},{"n":["penguin"],"u":"1f427"},{"n":["dove","dove of peace"],"u":"1f54a-fe0f"},{"n":["eagle"],"u":"1f985"},{"n":["duck"],"u":"1f986"},{"n":["swan"],"u":"1f9a2"},{"n":["owl"],"u":"1f989"},{"n":["dodo"],"u":"1f9a4"},{"n":["feather"],"u":"1fab6"},{"n":["flamingo"],"u":"1f9a9"},{"n":["peacock"],"u":"1f99a"},{"n":["parrot"],"u":"1f99c"},{"n":["frog","frog face"],"u":"1f438"},{"n":["crocodile"],"u":"1f40a"},{"n":["turtle"],"u":"1f422"},{"n":["lizard"],"u":"1f98e"},{"n":["snake"],"u":"1f40d"},{"n":["dragon face"],"u":"1f432"},{"n":["dragon"],"u":"1f409"},{"n":["sauropod"],"u":"1f995"},{"n":["t-rex"],"u":"1f996"},{"n":["whale","spouting whale"],"u":"1f433"},{"n":["whale","whale2"],"u":"1f40b"},{"n":["dolphin","flipper"],"u":"1f42c"},{"n":["seal"],"u":"1f9ad"},{"n":["fish"],"u":"1f41f"},{"n":["tropical fish"],"u":"1f420"},{"n":["blowfish"],"u":"1f421"},{"n":["shark"],"u":"1f988"},{"n":["octopus"],"u":"1f419"},{"n":["shell","spiral shell"],"u":"1f41a"},{"n":["coral"],"u":"1fab8"},{"n":["snail"],"u":"1f40c"},{"n":["butterfly"],"u":"1f98b"},{"n":["bug"],"u":"1f41b"},{"n":["ant"],"u":"1f41c"},{"n":["bee","honeybee"],"u":"1f41d"},{"n":["beetle"],"u":"1fab2"},{"n":["ladybug","lady beetle"],"u":"1f41e"},{"n":["cricket"],"u":"1f997"},{"n":["cockroach"],"u":"1fab3"},{"n":["spider"],"u":"1f577-fe0f"},{"n":["spider web"],"u":"1f578-fe0f"},{"n":["scorpion"],"u":"1f982"},{"n":["mosquito"],"u":"1f99f"},{"n":["fly"],"u":"1fab0"},{"n":["worm"],"u":"1fab1"},{"n":["microbe"],"u":"1f9a0"},{"n":["bouquet"],"u":"1f490"},{"n":["cherry blossom"],"u":"1f338"},{"n":["white flower"],"u":"1f4ae"},{"n":["lotus"],"u":"1fab7"},{"n":["rosette"],"u":"1f3f5-fe0f"},{"n":["rose"],"u":"1f339"},{"n":["wilted flower"],"u":"1f940"},{"n":["hibiscus"],"u":"1f33a"},{"n":["sunflower"],"u":"1f33b"},{"n":["blossom"],"u":"1f33c"},{"n":["tulip"],"u":"1f337"},{"n":["seedling"],"u":"1f331"},{"n":["potted plant"],"u":"1fab4"},{"n":["evergreen tree"],"u":"1f332"},{"n":["deciduous tree"],"u":"1f333"},{"n":["palm tree"],"u":"1f334"},{"n":["cactus"],"u":"1f335"},{"n":["ear of rice"],"u":"1f33e"},{"n":["herb"],"u":"1f33f"},{"n":["shamrock"],"u":"2618-fe0f"},{"n":["four leaf clover"],"u":"1f340"},{"n":["maple leaf"],"u":"1f341"},{"n":["fallen leaf"],"u":"1f342"},{"n":["leaves","leaf fluttering in wind"],"u":"1f343"},{"n":["empty nest"],"u":"1fab9"},{"n":["nest with eggs"],"u":"1faba"}],"food_drink":[{"n":["grapes"],"u":"1f347"},{"n":["melon"],"u":"1f348"},{"n":["watermelon"],"u":"1f349"},{"n":["tangerine"],"u":"1f34a"},{"n":["lemon"],"u":"1f34b"},{"n":["banana"],"u":"1f34c"},{"n":["pineapple"],"u":"1f34d"},{"n":["mango"],"u":"1f96d"},{"n":["apple","red apple"],"u":"1f34e"},{"n":["green apple"],"u":"1f34f"},{"n":["pear"],"u":"1f350"},{"n":["peach"],"u":"1f351"},{"n":["cherries"],"u":"1f352"},{"n":["strawberry"],"u":"1f353"},{"n":["blueberries"],"u":"1fad0"},{"n":["kiwifruit"],"u":"1f95d"},{"n":["tomato"],"u":"1f345"},{"n":["olive"],"u":"1fad2"},{"n":["coconut"],"u":"1f965"},{"n":["avocado"],"u":"1f951"},{"n":["eggplant","aubergine"],"u":"1f346"},{"n":["potato"],"u":"1f954"},{"n":["carrot"],"u":"1f955"},{"n":["corn","ear of maize"],"u":"1f33d"},{"n":["hot pepper"],"u":"1f336-fe0f"},{"n":["bell pepper"],"u":"1fad1"},{"n":["cucumber"],"u":"1f952"},{"n":["leafy green"],"u":"1f96c"},{"n":["broccoli"],"u":"1f966"},{"n":["garlic"],"u":"1f9c4"},{"n":["onion"],"u":"1f9c5"},{"n":["mushroom"],"u":"1f344"},{"n":["peanuts"],"u":"1f95c"},{"n":["beans"],"u":"1fad8"},{"n":["chestnut"],"u":"1f330"},{"n":["bread"],"u":"1f35e"},{"n":["croissant"],"u":"1f950"},{"n":["baguette bread"],"u":"1f956"},{"n":["flatbread"],"u":"1fad3"},{"n":["pretzel"],"u":"1f968"},{"n":["bagel"],"u":"1f96f"},{"n":["pancakes"],"u":"1f95e"},{"n":["waffle"],"u":"1f9c7"},{"n":["cheese wedge"],"u":"1f9c0"},{"n":["meat on bone"],"u":"1f356"},{"n":["poultry leg"],"u":"1f357"},{"n":["cut of meat"],"u":"1f969"},{"n":["bacon"],"u":"1f953"},{"n":["hamburger"],"u":"1f354"},{"n":["fries","french fries"],"u":"1f35f"},{"n":["pizza","slice of pizza"],"u":"1f355"},{"n":["hotdog","hot dog"],"u":"1f32d"},{"n":["sandwich"],"u":"1f96a"},{"n":["taco"],"u":"1f32e"},{"n":["burrito"],"u":"1f32f"},{"n":["tamale"],"u":"1fad4"},{"n":["stuffed flatbread"],"u":"1f959"},{"n":["falafel"],"u":"1f9c6"},{"n":["egg"],"u":"1f95a"},{"n":["cooking","fried egg"],"u":"1f373"},{"n":["shallow pan of food"],"u":"1f958"},{"n":["stew","pot of food"],"u":"1f372"},{"n":["fondue"],"u":"1fad5"},{"n":["bowl with spoon"],"u":"1f963"},{"n":["green salad"],"u":"1f957"},{"n":["popcorn"],"u":"1f37f"},{"n":["butter"],"u":"1f9c8"},{"n":["salt","salt shaker"],"u":"1f9c2"},{"n":["canned food"],"u":"1f96b"},{"n":["bento","bento box"],"u":"1f371"},{"n":["rice cracker"],"u":"1f358"},{"n":["rice ball"],"u":"1f359"},{"n":["rice","cooked rice"],"u":"1f35a"},{"n":["curry","curry and rice"],"u":"1f35b"},{"n":["ramen","steaming bowl"],"u":"1f35c"},{"n":["spaghetti"],"u":"1f35d"},{"n":["sweet potato","roasted sweet potato"],"u":"1f360"},{"n":["oden"],"u":"1f362"},{"n":["sushi"],"u":"1f363"},{"n":["fried shrimp"],"u":"1f364"},{"n":["fish cake","fish cake with swirl design"],"u":"1f365"},{"n":["moon cake"],"u":"1f96e"},{"n":["dango"],"u":"1f361"},{"n":["dumpling"],"u":"1f95f"},{"n":["fortune cookie"],"u":"1f960"},{"n":["takeout box"],"u":"1f961"},{"n":["crab"],"u":"1f980"},{"n":["lobster"],"u":"1f99e"},{"n":["shrimp"],"u":"1f990"},{"n":["squid"],"u":"1f991"},{"n":["oyster"],"u":"1f9aa"},{"n":["icecream","soft ice cream"],"u":"1f366"},{"n":["shaved ice"],"u":"1f367"},{"n":["ice cream"],"u":"1f368"},{"n":["doughnut"],"u":"1f369"},{"n":["cookie"],"u":"1f36a"},{"n":["birthday","birthday cake"],"u":"1f382"},{"n":["cake","shortcake"],"u":"1f370"},{"n":["cupcake"],"u":"1f9c1"},{"n":["pie"],"u":"1f967"},{"n":["chocolate bar"],"u":"1f36b"},{"n":["candy"],"u":"1f36c"},{"n":["lollipop"],"u":"1f36d"},{"n":["custard"],"u":"1f36e"},{"n":["honey pot"],"u":"1f36f"},{"n":["baby bottle"],"u":"1f37c"},{"n":["glass of milk"],"u":"1f95b"},{"n":["coffee","hot beverage"],"u":"2615"},{"n":["teapot"],"u":"1fad6"},{"n":["tea","teacup without handle"],"u":"1f375"},{"n":["sake","sake bottle and cup"],"u":"1f376"},{"n":["champagne","bottle with popping cork"],"u":"1f37e"},{"n":["wine glass"],"u":"1f377"},{"n":["cocktail","cocktail glass"],"u":"1f378"},{"n":["tropical drink"],"u":"1f379"},{"n":["beer","beer mug"],"u":"1f37a"},{"n":["beers","clinking beer mugs"],"u":"1f37b"},{"n":["clinking glasses"],"u":"1f942"},{"n":["tumbler glass"],"u":"1f943"},{"n":["pouring liquid"],"u":"1fad7"},{"n":["cup with straw"],"u":"1f964"},{"n":["bubble tea"],"u":"1f9cb"},{"n":["beverage box"],"u":"1f9c3"},{"n":["mate drink"],"u":"1f9c9"},{"n":["ice cube"],"u":"1f9ca"},{"n":["chopsticks"],"u":"1f962"},{"n":["knife fork plate","fork and knife with plate"],"u":"1f37d-fe0f"},{"n":["fork and knife"],"u":"1f374"},{"n":["spoon"],"u":"1f944"},{"n":["hocho","knife"],"u":"1f52a"},{"n":["jar"],"u":"1fad9"},{"n":["amphora"],"u":"1f3fa"}],"travel_places":[{"n":["earth africa","earth globe europe-africa"],"u":"1f30d"},{"n":["earth americas","earth globe americas"],"u":"1f30e"},{"n":["earth asia","earth globe asia-australia"],"u":"1f30f"},{"n":["globe with meridians"],"u":"1f310"},{"n":["world map"],"u":"1f5fa-fe0f"},{"n":["japan","silhouette of japan"],"u":"1f5fe"},{"n":["compass"],"u":"1f9ed"},{"n":["snow-capped mountain","snow capped mountain"],"u":"1f3d4-fe0f"},{"n":["mountain"],"u":"26f0-fe0f"},{"n":["volcano"],"u":"1f30b"},{"n":["mount fuji"],"u":"1f5fb"},{"n":["camping"],"u":"1f3d5-fe0f"},{"n":["beach with umbrella"],"u":"1f3d6-fe0f"},{"n":["desert"],"u":"1f3dc-fe0f"},{"n":["desert island"],"u":"1f3dd-fe0f"},{"n":["national park"],"u":"1f3de-fe0f"},{"n":["stadium"],"u":"1f3df-fe0f"},{"n":["classical building"],"u":"1f3db-fe0f"},{"n":["building construction"],"u":"1f3d7-fe0f"},{"n":["brick","bricks"],"u":"1f9f1"},{"n":["rock"],"u":"1faa8"},{"n":["wood"],"u":"1fab5"},{"n":["hut"],"u":"1f6d6"},{"n":["houses","house buildings"],"u":"1f3d8-fe0f"},{"n":["derelict house","derelict house building"],"u":"1f3da-fe0f"},{"n":["house","house building"],"u":"1f3e0"},{"n":["house with garden"],"u":"1f3e1"},{"n":["office","office building"],"u":"1f3e2"},{"n":["post office","japanese post office"],"u":"1f3e3"},{"n":["european post office"],"u":"1f3e4"},{"n":["hospital"],"u":"1f3e5"},{"n":["bank"],"u":"1f3e6"},{"n":["hotel"],"u":"1f3e8"},{"n":["love hotel"],"u":"1f3e9"},{"n":["convenience store"],"u":"1f3ea"},{"n":["school"],"u":"1f3eb"},{"n":["department store"],"u":"1f3ec"},{"n":["factory"],"u":"1f3ed"},{"n":["japanese castle"],"u":"1f3ef"},{"n":["european castle"],"u":"1f3f0"},{"n":["wedding"],"u":"1f492"},{"n":["tokyo tower"],"u":"1f5fc"},{"n":["statue of liberty"],"u":"1f5fd"},{"n":["church"],"u":"26ea"},{"n":["mosque"],"u":"1f54c"},{"n":["hindu temple"],"u":"1f6d5"},{"n":["synagogue"],"u":"1f54d"},{"n":["shinto shrine"],"u":"26e9-fe0f"},{"n":["kaaba"],"u":"1f54b"},{"n":["fountain"],"u":"26f2"},{"n":["tent"],"u":"26fa"},{"n":["foggy"],"u":"1f301"},{"n":["night with stars"],"u":"1f303"},{"n":["cityscape"],"u":"1f3d9-fe0f"},{"n":["sunrise over mountains"],"u":"1f304"},{"n":["sunrise"],"u":"1f305"},{"n":["city sunset","cityscape at dusk"],"u":"1f306"},{"n":["city sunrise","sunset over buildings"],"u":"1f307"},{"n":["bridge at night"],"u":"1f309"},{"n":["hotsprings","hot springs"],"u":"2668-fe0f"},{"n":["carousel horse"],"u":"1f3a0"},{"n":["playground slide"],"u":"1f6dd"},{"n":["ferris wheel"],"u":"1f3a1"},{"n":["roller coaster"],"u":"1f3a2"},{"n":["barber","barber pole"],"u":"1f488"},{"n":["circus tent"],"u":"1f3aa"},{"n":["steam locomotive"],"u":"1f682"},{"n":["railway car"],"u":"1f683"},{"n":["high-speed train","bullettrain side"],"u":"1f684"},{"n":["bullettrain front","high-speed train with bullet nose"],"u":"1f685"},{"n":["train","train2"],"u":"1f686"},{"n":["metro"],"u":"1f687"},{"n":["light rail"],"u":"1f688"},{"n":["station"],"u":"1f689"},{"n":["tram"],"u":"1f68a"},{"n":["monorail"],"u":"1f69d"},{"n":["mountain railway"],"u":"1f69e"},{"n":["train","tram car"],"u":"1f68b"},{"n":["bus"],"u":"1f68c"},{"n":["oncoming bus"],"u":"1f68d"},{"n":["trolleybus"],"u":"1f68e"},{"n":["minibus"],"u":"1f690"},{"n":["ambulance"],"u":"1f691"},{"n":["fire engine"],"u":"1f692"},{"n":["police car"],"u":"1f693"},{"n":["oncoming police car"],"u":"1f694"},{"n":["taxi"],"u":"1f695"},{"n":["oncoming taxi"],"u":"1f696"},{"n":["car","red car","automobile"],"u":"1f697"},{"n":["oncoming automobile"],"u":"1f698"},{"n":["blue car","recreational vehicle"],"u":"1f699"},{"n":["pickup truck"],"u":"1f6fb"},{"n":["truck","delivery truck"],"u":"1f69a"},{"n":["articulated lorry"],"u":"1f69b"},{"n":["tractor"],"u":"1f69c"},{"n":["racing car"],"u":"1f3ce-fe0f"},{"n":["motorcycle","racing motorcycle"],"u":"1f3cd-fe0f"},{"n":["motor scooter"],"u":"1f6f5"},{"n":["manual wheelchair"],"u":"1f9bd"},{"n":["motorized wheelchair"],"u":"1f9bc"},{"n":["auto rickshaw"],"u":"1f6fa"},{"n":["bike","bicycle"],"u":"1f6b2"},{"n":["scooter"],"u":"1f6f4"},{"n":["skateboard"],"u":"1f6f9"},{"n":["roller skate"],"u":"1f6fc"},{"n":["busstop","bus stop"],"u":"1f68f"},{"n":["motorway"],"u":"1f6e3-fe0f"},{"n":["railway track"],"u":"1f6e4-fe0f"},{"n":["oil drum"],"u":"1f6e2-fe0f"},{"n":["fuelpump","fuel pump"],"u":"26fd"},{"n":["wheel"],"u":"1f6de"},{"n":["rotating light","police cars revolving light"],"u":"1f6a8"},{"n":["traffic light","horizontal traffic light"],"u":"1f6a5"},{"n":["vertical traffic light"],"u":"1f6a6"},{"n":["octagonal sign"],"u":"1f6d1"},{"n":["construction","construction sign"],"u":"1f6a7"},{"n":["anchor"],"u":"2693"},{"n":["ring buoy"],"u":"1f6df"},{"n":["boat","sailboat"],"u":"26f5"},{"n":["canoe"],"u":"1f6f6"},{"n":["speedboat"],"u":"1f6a4"},{"n":["passenger ship"],"u":"1f6f3-fe0f"},{"n":["ferry"],"u":"26f4-fe0f"},{"n":["motor boat"],"u":"1f6e5-fe0f"},{"n":["ship"],"u":"1f6a2"},{"n":["airplane"],"u":"2708-fe0f"},{"n":["small airplane"],"u":"1f6e9-fe0f"},{"n":["airplane departure"],"u":"1f6eb"},{"n":["airplane arriving"],"u":"1f6ec"},{"n":["parachute"],"u":"1fa82"},{"n":["seat"],"u":"1f4ba"},{"n":["helicopter"],"u":"1f681"},{"n":["suspension railway"],"u":"1f69f"},{"n":["mountain cableway"],"u":"1f6a0"},{"n":["aerial tramway"],"u":"1f6a1"},{"n":["satellite"],"u":"1f6f0-fe0f"},{"n":["rocket"],"u":"1f680"},{"n":["flying saucer"],"u":"1f6f8"},{"n":["bellhop bell"],"u":"1f6ce-fe0f"},{"n":["luggage"],"u":"1f9f3"},{"n":["hourglass"],"u":"231b"},{"n":["hourglass flowing sand","hourglass with flowing sand"],"u":"23f3"},{"n":["watch"],"u":"231a"},{"n":["alarm clock"],"u":"23f0"},{"n":["stopwatch"],"u":"23f1-fe0f"},{"n":["timer clock"],"u":"23f2-fe0f"},{"n":["mantelpiece clock"],"u":"1f570-fe0f"},{"n":["clock12","clock face twelve oclock"],"u":"1f55b"},{"n":["clock1230","clock face twelve-thirty"],"u":"1f567"},{"n":["clock1","clock face one oclock"],"u":"1f550"},{"n":["clock130","clock face one-thirty"],"u":"1f55c"},{"n":["clock2","clock face two oclock"],"u":"1f551"},{"n":["clock230","clock face two-thirty"],"u":"1f55d"},{"n":["clock3","clock face three oclock"],"u":"1f552"},{"n":["clock330","clock face three-thirty"],"u":"1f55e"},{"n":["clock4","clock face four oclock"],"u":"1f553"},{"n":["clock430","clock face four-thirty"],"u":"1f55f"},{"n":["clock5","clock face five oclock"],"u":"1f554"},{"n":["clock530","clock face five-thirty"],"u":"1f560"},{"n":["clock6","clock face six oclock"],"u":"1f555"},{"n":["clock630","clock face six-thirty"],"u":"1f561"},{"n":["clock7","clock face seven oclock"],"u":"1f556"},{"n":["clock730","clock face seven-thirty"],"u":"1f562"},{"n":["clock8","clock face eight oclock"],"u":"1f557"},{"n":["clock830","clock face eight-thirty"],"u":"1f563"},{"n":["clock9","clock face nine oclock"],"u":"1f558"},{"n":["clock930","clock face nine-thirty"],"u":"1f564"},{"n":["clock10","clock face ten oclock"],"u":"1f559"},{"n":["clock1030","clock face ten-thirty"],"u":"1f565"},{"n":["clock11","clock face eleven oclock"],"u":"1f55a"},{"n":["clock1130","clock face eleven-thirty"],"u":"1f566"},{"n":["new moon","new moon symbol"],"u":"1f311"},{"n":["waxing crescent moon","waxing crescent moon symbol"],"u":"1f312"},{"n":["first quarter moon","first quarter moon symbol"],"u":"1f313"},{"n":["moon","waxing gibbous moon","waxing gibbous moon symbol"],"u":"1f314"},{"n":["full moon","full moon symbol"],"u":"1f315"},{"n":["waning gibbous moon","waning gibbous moon symbol"],"u":"1f316"},{"n":["last quarter moon","last quarter moon symbol"],"u":"1f317"},{"n":["waning crescent moon","waning crescent moon symbol"],"u":"1f318"},{"n":["crescent moon"],"u":"1f319"},{"n":["new moon with face"],"u":"1f31a"},{"n":["first quarter moon with face"],"u":"1f31b"},{"n":["last quarter moon with face"],"u":"1f31c"},{"n":["thermometer"],"u":"1f321-fe0f"},{"n":["sunny","black sun with rays"],"u":"2600-fe0f"},{"n":["full moon with face"],"u":"1f31d"},{"n":["sun with face"],"u":"1f31e"},{"n":["ringed planet"],"u":"1fa90"},{"n":["star","white medium star"],"u":"2b50"},{"n":["star2","glowing star"],"u":"1f31f"},{"n":["stars","shooting star"],"u":"1f320"},{"n":["milky way"],"u":"1f30c"},{"n":["cloud"],"u":"2601-fe0f"},{"n":["partly sunny","sun behind cloud"],"u":"26c5"},{"n":["thunder cloud and rain","cloud with lightning and rain"],"u":"26c8-fe0f"},{"n":["mostly sunny","sun small cloud","sun behind small cloud"],"u":"1f324-fe0f"},{"n":["barely sunny","sun behind cloud","sun behind large cloud"],"u":"1f325-fe0f"},{"n":["partly sunny rain","sun behind rain cloud"],"u":"1f326-fe0f"},{"n":["rain cloud","cloud with rain"],"u":"1f327-fe0f"},{"n":["snow cloud","cloud with snow"],"u":"1f328-fe0f"},{"n":["lightning","lightning cloud","cloud with lightning"],"u":"1f329-fe0f"},{"n":["tornado","tornado cloud"],"u":"1f32a-fe0f"},{"n":["fog"],"u":"1f32b-fe0f"},{"n":["wind face","wind blowing face"],"u":"1f32c-fe0f"},{"n":["cyclone"],"u":"1f300"},{"n":["rainbow"],"u":"1f308"},{"n":["closed umbrella"],"u":"1f302"},{"n":["umbrella"],"u":"2602-fe0f"},{"n":["umbrella with rain drops"],"u":"2614"},{"n":["umbrella on ground"],"u":"26f1-fe0f"},{"n":["zap","high voltage sign"],"u":"26a1"},{"n":["snowflake"],"u":"2744-fe0f"},{"n":["snowman"],"u":"2603-fe0f"},{"n":["snowman without snow"],"u":"26c4"},{"n":["comet"],"u":"2604-fe0f"},{"n":["fire"],"u":"1f525"},{"n":["droplet"],"u":"1f4a7"},{"n":["ocean","water wave"],"u":"1f30a"}],"activities":[{"n":["jack-o-lantern","jack o lantern"],"u":"1f383"},{"n":["christmas tree"],"u":"1f384"},{"n":["fireworks"],"u":"1f386"},{"n":["sparkler","firework sparkler"],"u":"1f387"},{"n":["firecracker"],"u":"1f9e8"},{"n":["sparkles"],"u":"2728"},{"n":["balloon"],"u":"1f388"},{"n":["tada","party popper"],"u":"1f389"},{"n":["confetti ball"],"u":"1f38a"},{"n":["tanabata tree"],"u":"1f38b"},{"n":["bamboo","pine decoration"],"u":"1f38d"},{"n":["dolls","japanese dolls"],"u":"1f38e"},{"n":["flags","carp streamer"],"u":"1f38f"},{"n":["wind chime"],"u":"1f390"},{"n":["rice scene","moon viewing ceremony"],"u":"1f391"},{"n":["red envelope","red gift envelope"],"u":"1f9e7"},{"n":["ribbon"],"u":"1f380"},{"n":["gift","wrapped present"],"u":"1f381"},{"n":["reminder ribbon"],"u":"1f397-fe0f"},{"n":["admission tickets"],"u":"1f39f-fe0f"},{"n":["ticket"],"u":"1f3ab"},{"n":["medal","military medal"],"u":"1f396-fe0f"},{"n":["trophy"],"u":"1f3c6"},{"n":["sports medal"],"u":"1f3c5"},{"n":["first place medal"],"u":"1f947"},{"n":["second place medal"],"u":"1f948"},{"n":["third place medal"],"u":"1f949"},{"n":["soccer","soccer ball"],"u":"26bd"},{"n":["baseball"],"u":"26be"},{"n":["softball"],"u":"1f94e"},{"n":["basketball","basketball and hoop"],"u":"1f3c0"},{"n":["volleyball"],"u":"1f3d0"},{"n":["football","american football"],"u":"1f3c8"},{"n":["rugby football"],"u":"1f3c9"},{"n":["tennis","tennis racquet and ball"],"u":"1f3be"},{"n":["flying disc"],"u":"1f94f"},{"n":["bowling"],"u":"1f3b3"},{"n":["cricket bat and ball"],"u":"1f3cf"},{"n":["field hockey stick and ball"],"u":"1f3d1"},{"n":["ice hockey stick and puck"],"u":"1f3d2"},{"n":["lacrosse","lacrosse stick and ball"],"u":"1f94d"},{"n":["table tennis paddle and ball"],"u":"1f3d3"},{"n":["badminton racquet and shuttlecock"],"u":"1f3f8"},{"n":["boxing glove"],"u":"1f94a"},{"n":["martial arts uniform"],"u":"1f94b"},{"n":["goal net"],"u":"1f945"},{"n":["golf","flag in hole"],"u":"26f3"},{"n":["ice skate"],"u":"26f8-fe0f"},{"n":["fishing pole and fish"],"u":"1f3a3"},{"n":["diving mask"],"u":"1f93f"},{"n":["running shirt with sash"],"u":"1f3bd"},{"n":["ski","ski and ski boot"],"u":"1f3bf"},{"n":["sled"],"u":"1f6f7"},{"n":["curling stone"],"u":"1f94c"},{"n":["dart","direct hit"],"u":"1f3af"},{"n":["yo-yo"],"u":"1fa80"},{"n":["kite"],"u":"1fa81"},{"n":["8ball","billiards"],"u":"1f3b1"},{"n":["crystal ball"],"u":"1f52e"},{"n":["magic wand"],"u":"1fa84"},{"n":["nazar amulet"],"u":"1f9ff"},{"n":["hamsa"],"u":"1faac"},{"n":["video game"],"u":"1f3ae"},{"n":["joystick"],"u":"1f579-fe0f"},{"n":["slot machine"],"u":"1f3b0"},{"n":["game die"],"u":"1f3b2"},{"n":["jigsaw","jigsaw puzzle piece"],"u":"1f9e9"},{"n":["teddy bear"],"u":"1f9f8"},{"n":["pinata"],"u":"1fa85"},{"n":["mirror ball"],"u":"1faa9"},{"n":["nesting dolls"],"u":"1fa86"},{"n":["spades","black spade suit"],"u":"2660-fe0f"},{"n":["hearts","black heart suit"],"u":"2665-fe0f"},{"n":["diamonds","black diamond suit"],"u":"2666-fe0f"},{"n":["clubs","black club suit"],"u":"2663-fe0f"},{"n":["chess pawn"],"u":"265f-fe0f"},{"n":["black joker","playing card black joker"],"u":"1f0cf"},{"n":["mahjong","mahjong tile red dragon"],"u":"1f004"},{"n":["flower playing cards"],"u":"1f3b4"},{"n":["performing arts"],"u":"1f3ad"},{"n":["framed picture","frame with picture"],"u":"1f5bc-fe0f"},{"n":["art","artist palette"],"u":"1f3a8"},{"n":["thread","spool of thread"],"u":"1f9f5"},{"n":["sewing needle"],"u":"1faa1"},{"n":["yarn","ball of yarn"],"u":"1f9f6"},{"n":["knot"],"u":"1faa2"}],"objects":[{"n":["eyeglasses"],"u":"1f453"},{"n":["sunglasses","dark sunglasses"],"u":"1f576-fe0f"},{"n":["goggles"],"u":"1f97d"},{"n":["lab coat"],"u":"1f97c"},{"n":["safety vest"],"u":"1f9ba"},{"n":["necktie"],"u":"1f454"},{"n":["shirt","tshirt","t-shirt"],"u":"1f455"},{"n":["jeans"],"u":"1f456"},{"n":["scarf"],"u":"1f9e3"},{"n":["gloves"],"u":"1f9e4"},{"n":["coat"],"u":"1f9e5"},{"n":["socks"],"u":"1f9e6"},{"n":["dress"],"u":"1f457"},{"n":["kimono"],"u":"1f458"},{"n":["sari"],"u":"1f97b"},{"n":["one-piece swimsuit"],"u":"1fa71"},{"n":["briefs"],"u":"1fa72"},{"n":["shorts"],"u":"1fa73"},{"n":["bikini"],"u":"1f459"},{"n":["womans clothes"],"u":"1f45a"},{"n":["purse"],"u":"1f45b"},{"n":["handbag"],"u":"1f45c"},{"n":["pouch"],"u":"1f45d"},{"n":["shopping bags"],"u":"1f6cd-fe0f"},{"n":["school satchel"],"u":"1f392"},{"n":["thong sandal"],"u":"1fa74"},{"n":["shoe","mans shoe"],"u":"1f45e"},{"n":["athletic shoe"],"u":"1f45f"},{"n":["hiking boot"],"u":"1f97e"},{"n":["flat shoe","womans flat shoe"],"u":"1f97f"},{"n":["high heel","high-heeled shoe"],"u":"1f460"},{"n":["sandal","womans sandal"],"u":"1f461"},{"n":["ballet shoes"],"u":"1fa70"},{"n":["boot","womans boots"],"u":"1f462"},{"n":["crown"],"u":"1f451"},{"n":["womans hat"],"u":"1f452"},{"n":["tophat","top hat"],"u":"1f3a9"},{"n":["mortar board","graduation cap"],"u":"1f393"},{"n":["billed cap"],"u":"1f9e2"},{"n":["military helmet"],"u":"1fa96"},{"n":["rescue worker’s helmet","helmet with white cross"],"u":"26d1-fe0f"},{"n":["prayer beads"],"u":"1f4ff"},{"n":["lipstick"],"u":"1f484"},{"n":["ring"],"u":"1f48d"},{"n":["gem","gem stone"],"u":"1f48e"},{"n":["mute","speaker with cancellation stroke"],"u":"1f507"},{"n":["speaker"],"u":"1f508"},{"n":["sound","speaker with one sound wave"],"u":"1f509"},{"n":["loud sound","speaker with three sound waves"],"u":"1f50a"},{"n":["loudspeaker","public address loudspeaker"],"u":"1f4e2"},{"n":["mega","cheering megaphone"],"u":"1f4e3"},{"n":["postal horn"],"u":"1f4ef"},{"n":["bell"],"u":"1f514"},{"n":["no bell","bell with cancellation stroke"],"u":"1f515"},{"n":["musical score"],"u":"1f3bc"},{"n":["musical note"],"u":"1f3b5"},{"n":["notes","multiple musical notes"],"u":"1f3b6"},{"n":["studio microphone"],"u":"1f399-fe0f"},{"n":["level slider"],"u":"1f39a-fe0f"},{"n":["control knobs"],"u":"1f39b-fe0f"},{"n":["microphone"],"u":"1f3a4"},{"n":["headphone","headphones"],"u":"1f3a7"},{"n":["radio"],"u":"1f4fb"},{"n":["saxophone"],"u":"1f3b7"},{"n":["accordion"],"u":"1fa97"},{"n":["guitar"],"u":"1f3b8"},{"n":["musical keyboard"],"u":"1f3b9"},{"n":["trumpet"],"u":"1f3ba"},{"n":["violin"],"u":"1f3bb"},{"n":["banjo"],"u":"1fa95"},{"n":["drum with drumsticks"],"u":"1f941"},{"n":["long drum"],"u":"1fa98"},{"n":["iphone","mobile phone"],"u":"1f4f1"},{"n":["calling","mobile phone with rightwards arrow at left"],"u":"1f4f2"},{"n":["phone","telephone","black telephone"],"u":"260e-fe0f"},{"n":["telephone receiver"],"u":"1f4de"},{"n":["pager"],"u":"1f4df"},{"n":["fax","fax machine"],"u":"1f4e0"},{"n":["battery"],"u":"1f50b"},{"n":["low battery"],"u":"1faab"},{"n":["electric plug"],"u":"1f50c"},{"n":["computer","personal computer"],"u":"1f4bb"},{"n":["desktop computer"],"u":"1f5a5-fe0f"},{"n":["printer"],"u":"1f5a8-fe0f"},{"n":["keyboard"],"u":"2328-fe0f"},{"n":["computer mouse","three button mouse"],"u":"1f5b1-fe0f"},{"n":["trackball"],"u":"1f5b2-fe0f"},{"n":["minidisc"],"u":"1f4bd"},{"n":["floppy disk"],"u":"1f4be"},{"n":["cd","optical disc"],"u":"1f4bf"},{"n":["dvd"],"u":"1f4c0"},{"n":["abacus"],"u":"1f9ee"},{"n":["movie camera"],"u":"1f3a5"},{"n":["film frames"],"u":"1f39e-fe0f"},{"n":["film projector"],"u":"1f4fd-fe0f"},{"n":["clapper","clapper board"],"u":"1f3ac"},{"n":["tv","television"],"u":"1f4fa"},{"n":["camera"],"u":"1f4f7"},{"n":["camera with flash"],"u":"1f4f8"},{"n":["video camera"],"u":"1f4f9"},{"n":["vhs","videocassette"],"u":"1f4fc"},{"n":["mag","left-pointing magnifying glass"],"u":"1f50d"},{"n":["mag right","right-pointing magnifying glass"],"u":"1f50e"},{"n":["candle"],"u":"1f56f-fe0f"},{"n":["bulb","electric light bulb"],"u":"1f4a1"},{"n":["flashlight","electric torch"],"u":"1f526"},{"n":["lantern","izakaya lantern"],"u":"1f3ee"},{"n":["diya lamp"],"u":"1fa94"},{"n":["notebook with decorative cover"],"u":"1f4d4"},{"n":["closed book"],"u":"1f4d5"},{"n":["book","open book"],"u":"1f4d6"},{"n":["green book"],"u":"1f4d7"},{"n":["blue book"],"u":"1f4d8"},{"n":["orange book"],"u":"1f4d9"},{"n":["books"],"u":"1f4da"},{"n":["notebook"],"u":"1f4d3"},{"n":["ledger"],"u":"1f4d2"},{"n":["page with curl"],"u":"1f4c3"},{"n":["scroll"],"u":"1f4dc"},{"n":["page facing up"],"u":"1f4c4"},{"n":["newspaper"],"u":"1f4f0"},{"n":["rolled-up newspaper","rolled up newspaper"],"u":"1f5de-fe0f"},{"n":["bookmark tabs"],"u":"1f4d1"},{"n":["bookmark"],"u":"1f516"},{"n":["label"],"u":"1f3f7-fe0f"},{"n":["moneybag","money bag"],"u":"1f4b0"},{"n":["coin"],"u":"1fa99"},{"n":["yen","banknote with yen sign"],"u":"1f4b4"},{"n":["dollar","banknote with dollar sign"],"u":"1f4b5"},{"n":["euro","banknote with euro sign"],"u":"1f4b6"},{"n":["pound","banknote with pound sign"],"u":"1f4b7"},{"n":["money with wings"],"u":"1f4b8"},{"n":["credit card"],"u":"1f4b3"},{"n":["receipt"],"u":"1f9fe"},{"n":["chart","chart with upwards trend and yen sign"],"u":"1f4b9"},{"n":["email","envelope"],"u":"2709-fe0f"},{"n":["e-mail","e-mail symbol"],"u":"1f4e7"},{"n":["incoming envelope"],"u":"1f4e8"},{"n":["envelope with arrow","envelope with downwards arrow above"],"u":"1f4e9"},{"n":["outbox tray"],"u":"1f4e4"},{"n":["inbox tray"],"u":"1f4e5"},{"n":["package"],"u":"1f4e6"},{"n":["mailbox","closed mailbox with raised flag"],"u":"1f4eb"},{"n":["mailbox closed","closed mailbox with lowered flag"],"u":"1f4ea"},{"n":["mailbox with mail","open mailbox with raised flag"],"u":"1f4ec"},{"n":["mailbox with no mail","open mailbox with lowered flag"],"u":"1f4ed"},{"n":["postbox"],"u":"1f4ee"},{"n":["ballot box with ballot"],"u":"1f5f3-fe0f"},{"n":["pencil","pencil2"],"u":"270f-fe0f"},{"n":["black nib"],"u":"2712-fe0f"},{"n":["fountain pen","lower left fountain pen"],"u":"1f58b-fe0f"},{"n":["pen","lower left ballpoint pen"],"u":"1f58a-fe0f"},{"n":["paintbrush","lower left paintbrush"],"u":"1f58c-fe0f"},{"n":["crayon","lower left crayon"],"u":"1f58d-fe0f"},{"n":["memo","pencil"],"u":"1f4dd"},{"n":["briefcase"],"u":"1f4bc"},{"n":["file folder"],"u":"1f4c1"},{"n":["open file folder"],"u":"1f4c2"},{"n":["card index dividers"],"u":"1f5c2-fe0f"},{"n":["date","calendar"],"u":"1f4c5"},{"n":["calendar","tear-off calendar"],"u":"1f4c6"},{"n":["spiral notepad","spiral note pad"],"u":"1f5d2-fe0f"},{"n":["spiral calendar","spiral calendar pad"],"u":"1f5d3-fe0f"},{"n":["card index"],"u":"1f4c7"},{"n":["chart with upwards trend"],"u":"1f4c8"},{"n":["chart with downwards trend"],"u":"1f4c9"},{"n":["bar chart"],"u":"1f4ca"},{"n":["clipboard"],"u":"1f4cb"},{"n":["pushpin"],"u":"1f4cc"},{"n":["round pushpin"],"u":"1f4cd"},{"n":["paperclip"],"u":"1f4ce"},{"n":["linked paperclips"],"u":"1f587-fe0f"},{"n":["straight ruler"],"u":"1f4cf"},{"n":["triangular ruler"],"u":"1f4d0"},{"n":["scissors","black scissors"],"u":"2702-fe0f"},{"n":["card file box"],"u":"1f5c3-fe0f"},{"n":["file cabinet"],"u":"1f5c4-fe0f"},{"n":["wastebasket"],"u":"1f5d1-fe0f"},{"n":["lock"],"u":"1f512"},{"n":["unlock","open lock"],"u":"1f513"},{"n":["lock with ink pen"],"u":"1f50f"},{"n":["closed lock with key"],"u":"1f510"},{"n":["key"],"u":"1f511"},{"n":["old key"],"u":"1f5dd-fe0f"},{"n":["hammer"],"u":"1f528"},{"n":["axe"],"u":"1fa93"},{"n":["pick"],"u":"26cf-fe0f"},{"n":["hammer and pick"],"u":"2692-fe0f"},{"n":["hammer and wrench"],"u":"1f6e0-fe0f"},{"n":["dagger","dagger knife"],"u":"1f5e1-fe0f"},{"n":["crossed swords"],"u":"2694-fe0f"},{"n":["gun","pistol"],"u":"1f52b"},{"n":["boomerang"],"u":"1fa83"},{"n":["bow and arrow"],"u":"1f3f9"},{"n":["shield"],"u":"1f6e1-fe0f"},{"n":["carpentry saw"],"u":"1fa9a"},{"n":["wrench"],"u":"1f527"},{"n":["screwdriver"],"u":"1fa9b"},{"n":["nut and bolt"],"u":"1f529"},{"n":["gear"],"u":"2699-fe0f"},{"n":["clamp","compression"],"u":"1f5dc-fe0f"},{"n":["scales","balance scale"],"u":"2696-fe0f"},{"n":["probing cane"],"u":"1f9af"},{"n":["link","link symbol"],"u":"1f517"},{"n":["chains"],"u":"26d3-fe0f"},{"n":["hook"],"u":"1fa9d"},{"n":["toolbox"],"u":"1f9f0"},{"n":["magnet"],"u":"1f9f2"},{"n":["ladder"],"u":"1fa9c"},{"n":["alembic"],"u":"2697-fe0f"},{"n":["test tube"],"u":"1f9ea"},{"n":["petri dish"],"u":"1f9eb"},{"n":["dna","dna double helix"],"u":"1f9ec"},{"n":["microscope"],"u":"1f52c"},{"n":["telescope"],"u":"1f52d"},{"n":["satellite antenna"],"u":"1f4e1"},{"n":["syringe"],"u":"1f489"},{"n":["drop of blood"],"u":"1fa78"},{"n":["pill"],"u":"1f48a"},{"n":["adhesive bandage"],"u":"1fa79"},{"n":["crutch"],"u":"1fa7c"},{"n":["stethoscope"],"u":"1fa7a"},{"n":["x-ray"],"u":"1fa7b"},{"n":["door"],"u":"1f6aa"},{"n":["elevator"],"u":"1f6d7"},{"n":["mirror"],"u":"1fa9e"},{"n":["window"],"u":"1fa9f"},{"n":["bed"],"u":"1f6cf-fe0f"},{"n":["couch and lamp"],"u":"1f6cb-fe0f"},{"n":["chair"],"u":"1fa91"},{"n":["toilet"],"u":"1f6bd"},{"n":["plunger"],"u":"1faa0"},{"n":["shower"],"u":"1f6bf"},{"n":["bathtub"],"u":"1f6c1"},{"n":["mouse trap"],"u":"1faa4"},{"n":["razor"],"u":"1fa92"},{"n":["lotion bottle"],"u":"1f9f4"},{"n":["safety pin"],"u":"1f9f7"},{"n":["broom"],"u":"1f9f9"},{"n":["basket"],"u":"1f9fa"},{"n":["roll of paper"],"u":"1f9fb"},{"n":["bucket"],"u":"1faa3"},{"n":["soap","bar of soap"],"u":"1f9fc"},{"n":["bubbles"],"u":"1fae7"},{"n":["toothbrush"],"u":"1faa5"},{"n":["sponge"],"u":"1f9fd"},{"n":["fire extinguisher"],"u":"1f9ef"},{"n":["shopping trolley"],"u":"1f6d2"},{"n":["smoking","smoking symbol"],"u":"1f6ac"},{"n":["coffin"],"u":"26b0-fe0f"},{"n":["headstone"],"u":"1faa6"},{"n":["funeral urn"],"u":"26b1-fe0f"},{"n":["moyai"],"u":"1f5ff"},{"n":["placard"],"u":"1faa7"},{"n":["identification card"],"u":"1faaa"}],"symbols":[{"n":["atm","automated teller machine"],"u":"1f3e7"},{"n":["put litter in its place","put litter in its place symbol"],"u":"1f6ae"},{"n":["potable water","potable water symbol"],"u":"1f6b0"},{"n":["wheelchair","wheelchair symbol"],"u":"267f"},{"n":["mens","mens symbol"],"u":"1f6b9"},{"n":["womens","womens symbol"],"u":"1f6ba"},{"n":["restroom"],"u":"1f6bb"},{"n":["baby symbol"],"u":"1f6bc"},{"n":["wc","water closet"],"u":"1f6be"},{"n":["passport control"],"u":"1f6c2"},{"n":["customs"],"u":"1f6c3"},{"n":["baggage claim"],"u":"1f6c4"},{"n":["left luggage"],"u":"1f6c5"},{"n":["warning","warning sign"],"u":"26a0-fe0f"},{"n":["children crossing"],"u":"1f6b8"},{"n":["no entry"],"u":"26d4"},{"n":["no entry sign"],"u":"1f6ab"},{"n":["no bicycles"],"u":"1f6b3"},{"n":["no smoking","no smoking symbol"],"u":"1f6ad"},{"n":["do not litter","do not litter symbol"],"u":"1f6af"},{"n":["non-potable water","non-potable water symbol"],"u":"1f6b1"},{"n":["no pedestrians"],"u":"1f6b7"},{"n":["no mobile phones"],"u":"1f4f5"},{"n":["underage","no one under eighteen symbol"],"u":"1f51e"},{"n":["radioactive","radioactive sign"],"u":"2622-fe0f"},{"n":["biohazard","biohazard sign"],"u":"2623-fe0f"},{"n":["arrow up","upwards black arrow"],"u":"2b06-fe0f"},{"n":["north east arrow","arrow upper right"],"u":"2197-fe0f"},{"n":["arrow right","black rightwards arrow"],"u":"27a1-fe0f"},{"n":["south east arrow","arrow lower right"],"u":"2198-fe0f"},{"n":["arrow down","downwards black arrow"],"u":"2b07-fe0f"},{"n":["south west arrow","arrow lower left"],"u":"2199-fe0f"},{"n":["arrow left","leftwards black arrow"],"u":"2b05-fe0f"},{"n":["north west arrow","arrow upper left"],"u":"2196-fe0f"},{"n":["up down arrow","arrow up down"],"u":"2195-fe0f"},{"n":["left right arrow"],"u":"2194-fe0f"},{"n":["leftwards arrow with hook"],"u":"21a9-fe0f"},{"n":["arrow right hook","rightwards arrow with hook"],"u":"21aa-fe0f"},{"n":["arrow heading up","arrow pointing rightwards then curving upwards"],"u":"2934-fe0f"},{"n":["arrow heading down","arrow pointing rightwards then curving downwards"],"u":"2935-fe0f"},{"n":["arrows clockwise","clockwise downwards and upwards open circle arrows"],"u":"1f503"},{"n":["arrows counterclockwise","anticlockwise downwards and upwards open circle arrows"],"u":"1f504"},{"n":["back","back with leftwards arrow above"],"u":"1f519"},{"n":["end","end with leftwards arrow above"],"u":"1f51a"},{"n":["on","on with exclamation mark with left right arrow above"],"u":"1f51b"},{"n":["soon","soon with rightwards arrow above"],"u":"1f51c"},{"n":["top","top with upwards arrow above"],"u":"1f51d"},{"n":["place of worship"],"u":"1f6d0"},{"n":["atom symbol"],"u":"269b-fe0f"},{"n":["om","om symbol"],"u":"1f549-fe0f"},{"n":["star of david"],"u":"2721-fe0f"},{"n":["wheel of dharma"],"u":"2638-fe0f"},{"n":["yin yang"],"u":"262f-fe0f"},{"n":["latin cross"],"u":"271d-fe0f"},{"n":["orthodox cross"],"u":"2626-fe0f"},{"n":["star and crescent"],"u":"262a-fe0f"},{"n":["peace symbol"],"u":"262e-fe0f"},{"n":["menorah with nine branches"],"u":"1f54e"},{"n":["six pointed star","six pointed star with middle dot"],"u":"1f52f"},{"n":["aries"],"u":"2648"},{"n":["taurus"],"u":"2649"},{"n":["gemini"],"u":"264a"},{"n":["cancer"],"u":"264b"},{"n":["leo"],"u":"264c"},{"n":["virgo"],"u":"264d"},{"n":["libra"],"u":"264e"},{"n":["scorpius"],"u":"264f"},{"n":["sagittarius"],"u":"2650"},{"n":["capricorn"],"u":"2651"},{"n":["aquarius"],"u":"2652"},{"n":["pisces"],"u":"2653"},{"n":["ophiuchus"],"u":"26ce"},{"n":["twisted rightwards arrows"],"u":"1f500"},{"n":["repeat","clockwise rightwards and leftwards open circle arrows"],"u":"1f501"},{"n":["repeat one","clockwise rightwards and leftwards open circle arrows with circled one overlay"],"u":"1f502"},{"n":["arrow forward","black right-pointing triangle"],"u":"25b6-fe0f"},{"n":["fast forward","black right-pointing double triangle"],"u":"23e9"},{"n":["next track button","black right pointing double triangle with vertical bar"],"u":"23ed-fe0f"},{"n":["play or pause button","black right pointing triangle with double vertical bar"],"u":"23ef-fe0f"},{"n":["arrow backward","black left-pointing triangle"],"u":"25c0-fe0f"},{"n":["rewind","black left-pointing double triangle"],"u":"23ea"},{"n":["last track button","black left pointing double triangle with vertical bar"],"u":"23ee-fe0f"},{"n":["arrow up small","up-pointing small red triangle"],"u":"1f53c"},{"n":["arrow double up","black up-pointing double triangle"],"u":"23eb"},{"n":["arrow down small","down-pointing small red triangle"],"u":"1f53d"},{"n":["arrow double down","black down-pointing double triangle"],"u":"23ec"},{"n":["pause button","double vertical bar"],"u":"23f8-fe0f"},{"n":["stop button","black square for stop"],"u":"23f9-fe0f"},{"n":["record button","black circle for record"],"u":"23fa-fe0f"},{"n":["eject","eject button"],"u":"23cf-fe0f"},{"n":["cinema"],"u":"1f3a6"},{"n":["low brightness","low brightness symbol"],"u":"1f505"},{"n":["high brightness","high brightness symbol"],"u":"1f506"},{"n":["signal strength","antenna with bars"],"u":"1f4f6"},{"n":["vibration mode"],"u":"1f4f3"},{"n":["mobile phone off"],"u":"1f4f4"},{"n":["female sign"],"u":"2640-fe0f"},{"n":["male sign"],"u":"2642-fe0f"},{"n":["transgender symbol"],"u":"26a7-fe0f"},{"n":["heavy multiplication x"],"u":"2716-fe0f"},{"n":["heavy plus sign"],"u":"2795"},{"n":["heavy minus sign"],"u":"2796"},{"n":["heavy division sign"],"u":"2797"},{"n":["heavy equals sign"],"u":"1f7f0"},{"n":["infinity"],"u":"267e-fe0f"},{"n":["bangbang","double exclamation mark"],"u":"203c-fe0f"},{"n":["interrobang","exclamation question mark"],"u":"2049-fe0f"},{"n":["question","black question mark ornament"],"u":"2753"},{"n":["grey question","white question mark ornament"],"u":"2754"},{"n":["grey exclamation","white exclamation mark ornament"],"u":"2755"},{"n":["exclamation","heavy exclamation mark","heavy exclamation mark symbol"],"u":"2757"},{"n":["wavy dash"],"u":"3030-fe0f"},{"n":["currency exchange"],"u":"1f4b1"},{"n":["heavy dollar sign"],"u":"1f4b2"},{"n":["medical symbol","staff of aesculapius"],"u":"2695-fe0f"},{"n":["recycle","black universal recycling symbol"],"u":"267b-fe0f"},{"n":["fleur-de-lis","fleur de lis"],"u":"269c-fe0f"},{"n":["trident","trident emblem"],"u":"1f531"},{"n":["name badge"],"u":"1f4db"},{"n":["beginner","japanese symbol for beginner"],"u":"1f530"},{"n":["o","heavy large circle"],"u":"2b55"},{"n":["white check mark","white heavy check mark"],"u":"2705"},{"n":["ballot box with check"],"u":"2611-fe0f"},{"n":["heavy check mark"],"u":"2714-fe0f"},{"n":["x","cross mark"],"u":"274c"},{"n":["negative squared cross mark"],"u":"274e"},{"n":["curly loop"],"u":"27b0"},{"n":["loop","double curly loop"],"u":"27bf"},{"n":["part alternation mark"],"u":"303d-fe0f"},{"n":["eight spoked asterisk"],"u":"2733-fe0f"},{"n":["eight pointed black star"],"u":"2734-fe0f"},{"n":["sparkle"],"u":"2747-fe0f"},{"n":["copyright","copyright sign"],"u":"00a9-fe0f"},{"n":["registered","registered sign"],"u":"00ae-fe0f"},{"n":["tm","trade mark sign"],"u":"2122-fe0f"},{"n":["hash","hash key"],"u":"0023-fe0f-20e3"},{"n":["keycap: *","keycap star"],"u":"002a-fe0f-20e3"},{"n":["zero","keycap 0"],"u":"0030-fe0f-20e3"},{"n":["one","keycap 1"],"u":"0031-fe0f-20e3"},{"n":["two","keycap 2"],"u":"0032-fe0f-20e3"},{"n":["three","keycap 3"],"u":"0033-fe0f-20e3"},{"n":["four","keycap 4"],"u":"0034-fe0f-20e3"},{"n":["five","keycap 5"],"u":"0035-fe0f-20e3"},{"n":["six","keycap 6"],"u":"0036-fe0f-20e3"},{"n":["seven","keycap 7"],"u":"0037-fe0f-20e3"},{"n":["eight","keycap 8"],"u":"0038-fe0f-20e3"},{"n":["nine","keycap 9"],"u":"0039-fe0f-20e3"},{"n":["keycap ten"],"u":"1f51f"},{"n":["capital abcd","input symbol for latin capital letters"],"u":"1f520"},{"n":["abcd","input symbol for latin small letters"],"u":"1f521"},{"n":["1234","input symbol for numbers"],"u":"1f522"},{"n":["symbols","input symbol for symbols"],"u":"1f523"},{"n":["abc","input symbol for latin letters"],"u":"1f524"},{"n":["a","negative squared latin capital letter a"],"u":"1f170-fe0f"},{"n":["ab","negative squared ab"],"u":"1f18e"},{"n":["b","negative squared latin capital letter b"],"u":"1f171-fe0f"},{"n":["cl","squared cl"],"u":"1f191"},{"n":["cool","squared cool"],"u":"1f192"},{"n":["free","squared free"],"u":"1f193"},{"n":["information source"],"u":"2139-fe0f"},{"n":["id","squared id"],"u":"1f194"},{"n":["m","circled latin capital letter m"],"u":"24c2-fe0f"},{"n":["new","squared new"],"u":"1f195"},{"n":["ng","squared ng"],"u":"1f196"},{"n":["o2","negative squared latin capital letter o"],"u":"1f17e-fe0f"},{"n":["ok","squared ok"],"u":"1f197"},{"n":["parking","negative squared latin capital letter p"],"u":"1f17f-fe0f"},{"n":["sos","squared sos"],"u":"1f198"},{"n":["up","squared up with exclamation mark"],"u":"1f199"},{"n":["vs","squared vs"],"u":"1f19a"},{"n":["koko","squared katakana koko"],"u":"1f201"},{"n":["sa","squared katakana sa"],"u":"1f202-fe0f"},{"n":["u6708","squared cjk unified ideograph-6708"],"u":"1f237-fe0f"},{"n":["u6709","squared cjk unified ideograph-6709"],"u":"1f236"},{"n":["u6307","squared cjk unified ideograph-6307"],"u":"1f22f"},{"n":["ideograph advantage","circled ideograph advantage"],"u":"1f250"},{"n":["u5272","squared cjk unified ideograph-5272"],"u":"1f239"},{"n":["u7121","squared cjk unified ideograph-7121"],"u":"1f21a"},{"n":["u7981","squared cjk unified ideograph-7981"],"u":"1f232"},{"n":["accept","circled ideograph accept"],"u":"1f251"},{"n":["u7533","squared cjk unified ideograph-7533"],"u":"1f238"},{"n":["u5408","squared cjk unified ideograph-5408"],"u":"1f234"},{"n":["u7a7a","squared cjk unified ideograph-7a7a"],"u":"1f233"},{"n":["congratulations","circled ideograph congratulation"],"u":"3297-fe0f"},{"n":["secret","circled ideograph secret"],"u":"3299-fe0f"},{"n":["u55b6","squared cjk unified ideograph-55b6"],"u":"1f23a"},{"n":["u6e80","squared cjk unified ideograph-6e80"],"u":"1f235"},{"n":["red circle","large red circle"],"u":"1f534"},{"n":["large orange circle"],"u":"1f7e0"},{"n":["large yellow circle"],"u":"1f7e1"},{"n":["large green circle"],"u":"1f7e2"},{"n":["large blue circle"],"u":"1f535"},{"n":["large purple circle"],"u":"1f7e3"},{"n":["large brown circle"],"u":"1f7e4"},{"n":["black circle","medium black circle"],"u":"26ab"},{"n":["white circle","medium white circle"],"u":"26aa"},{"n":["large red square"],"u":"1f7e5"},{"n":["large orange square"],"u":"1f7e7"},{"n":["large yellow square"],"u":"1f7e8"},{"n":["large green square"],"u":"1f7e9"},{"n":["large blue square"],"u":"1f7e6"},{"n":["large purple square"],"u":"1f7ea"},{"n":["large brown square"],"u":"1f7eb"},{"n":["black large square"],"u":"2b1b"},{"n":["white large square"],"u":"2b1c"},{"n":["black medium square"],"u":"25fc-fe0f"},{"n":["white medium square"],"u":"25fb-fe0f"},{"n":["black medium small square"],"u":"25fe"},{"n":["white medium small square"],"u":"25fd"},{"n":["black small square"],"u":"25aa-fe0f"},{"n":["white small square"],"u":"25ab-fe0f"},{"n":["large orange diamond"],"u":"1f536"},{"n":["large blue diamond"],"u":"1f537"},{"n":["small orange diamond"],"u":"1f538"},{"n":["small blue diamond"],"u":"1f539"},{"n":["small red triangle","up-pointing red triangle"],"u":"1f53a"},{"n":["small red triangle down","down-pointing red triangle"],"u":"1f53b"},{"n":["diamond shape with a dot inside"],"u":"1f4a0"},{"n":["radio button"],"u":"1f518"},{"n":["white square button"],"u":"1f533"},{"n":["black square button"],"u":"1f532"}],"flags":[{"n":["chequered flag","checkered flag"],"u":"1f3c1"},{"n":["triangular flag on post"],"u":"1f6a9"},{"n":["crossed flags"],"u":"1f38c"},{"n":["waving black flag"],"u":"1f3f4"},{"n":["white flag","waving white flag"],"u":"1f3f3-fe0f"},{"n":["rainbow flag","rainbow-flag"],"u":"1f3f3-fe0f-200d-1f308"},{"n":["transgender flag"],"u":"1f3f3-fe0f-200d-26a7-fe0f"},{"n":["pirate flag"],"u":"1f3f4-200d-2620-fe0f"},{"n":["flag-ac","ascension island flag"],"u":"1f1e6-1f1e8"},{"n":["flag-ad","andorra flag"],"u":"1f1e6-1f1e9"},{"n":["flag-ae","united arab emirates flag"],"u":"1f1e6-1f1ea"},{"n":["flag-af","afghanistan flag"],"u":"1f1e6-1f1eb"},{"n":["flag-ag","antigua & barbuda flag"],"u":"1f1e6-1f1ec"},{"n":["flag-ai","anguilla flag"],"u":"1f1e6-1f1ee"},{"n":["flag-al","albania flag"],"u":"1f1e6-1f1f1"},{"n":["flag-am","armenia flag"],"u":"1f1e6-1f1f2"},{"n":["flag-ao","angola flag"],"u":"1f1e6-1f1f4"},{"n":["flag-aq","antarctica flag"],"u":"1f1e6-1f1f6"},{"n":["flag-ar","argentina flag"],"u":"1f1e6-1f1f7"},{"n":["flag-as","american samoa flag"],"u":"1f1e6-1f1f8"},{"n":["flag-at","austria flag"],"u":"1f1e6-1f1f9"},{"n":["flag-au","australia flag"],"u":"1f1e6-1f1fa"},{"n":["flag-aw","aruba flag"],"u":"1f1e6-1f1fc"},{"n":["flag-ax","Ã¥land islands flag"],"u":"1f1e6-1f1fd"},{"n":["flag-az","azerbaijan flag"],"u":"1f1e6-1f1ff"},{"n":["flag-ba","bosnia & herzegovina flag"],"u":"1f1e7-1f1e6"},{"n":["flag-bb","barbados flag"],"u":"1f1e7-1f1e7"},{"n":["flag-bd","bangladesh flag"],"u":"1f1e7-1f1e9"},{"n":["flag-be","belgium flag"],"u":"1f1e7-1f1ea"},{"n":["flag-bf","burkina faso flag"],"u":"1f1e7-1f1eb"},{"n":["flag-bg","bulgaria flag"],"u":"1f1e7-1f1ec"},{"n":["flag-bh","bahrain flag"],"u":"1f1e7-1f1ed"},{"n":["flag-bi","burundi flag"],"u":"1f1e7-1f1ee"},{"n":["flag-bj","benin flag"],"u":"1f1e7-1f1ef"},{"n":["flag-bl","st. barthélemy flag"],"u":"1f1e7-1f1f1"},{"n":["flag-bm","bermuda flag"],"u":"1f1e7-1f1f2"},{"n":["flag-bn","brunei flag"],"u":"1f1e7-1f1f3"},{"n":["flag-bo","bolivia flag"],"u":"1f1e7-1f1f4"},{"n":["flag-bq","caribbean netherlands flag"],"u":"1f1e7-1f1f6"},{"n":["flag-br","brazil flag"],"u":"1f1e7-1f1f7"},{"n":["flag-bs","bahamas flag"],"u":"1f1e7-1f1f8"},{"n":["flag-bt","bhutan flag"],"u":"1f1e7-1f1f9"},{"n":["flag-bv","bouvet island flag"],"u":"1f1e7-1f1fb"},{"n":["flag-bw","botswana flag"],"u":"1f1e7-1f1fc"},{"n":["flag-by","belarus flag"],"u":"1f1e7-1f1fe"},{"n":["flag-bz","belize flag"],"u":"1f1e7-1f1ff"},{"n":["flag-ca","canada flag"],"u":"1f1e8-1f1e6"},{"n":["flag-cc","cocos (keeling) islands flag"],"u":"1f1e8-1f1e8"},{"n":["flag-cd","congo - kinshasa flag"],"u":"1f1e8-1f1e9"},{"n":["flag-cf","central african republic flag"],"u":"1f1e8-1f1eb"},{"n":["flag-cg","congo - brazzaville flag"],"u":"1f1e8-1f1ec"},{"n":["flag-ch","switzerland flag"],"u":"1f1e8-1f1ed"},{"n":["flag-ci","côte d’ivoire flag"],"u":"1f1e8-1f1ee"},{"n":["flag-ck","cook islands flag"],"u":"1f1e8-1f1f0"},{"n":["flag-cl","chile flag"],"u":"1f1e8-1f1f1"},{"n":["flag-cm","cameroon flag"],"u":"1f1e8-1f1f2"},{"n":["cn","flag-cn","china flag"],"u":"1f1e8-1f1f3"},{"n":["flag-co","colombia flag"],"u":"1f1e8-1f1f4"},{"n":["flag-cp","clipperton island flag"],"u":"1f1e8-1f1f5"},{"n":["flag-cr","costa rica flag"],"u":"1f1e8-1f1f7"},{"n":["flag-cu","cuba flag"],"u":"1f1e8-1f1fa"},{"n":["flag-cv","cape verde flag"],"u":"1f1e8-1f1fb"},{"n":["flag-cw","curaçao flag"],"u":"1f1e8-1f1fc"},{"n":["flag-cx","christmas island flag"],"u":"1f1e8-1f1fd"},{"n":["flag-cy","cyprus flag"],"u":"1f1e8-1f1fe"},{"n":["flag-cz","czechia flag"],"u":"1f1e8-1f1ff"},{"n":["de","flag-de","germany flag"],"u":"1f1e9-1f1ea"},{"n":["flag-dg","diego garcia flag"],"u":"1f1e9-1f1ec"},{"n":["flag-dj","djibouti flag"],"u":"1f1e9-1f1ef"},{"n":["flag-dk","denmark flag"],"u":"1f1e9-1f1f0"},{"n":["flag-dm","dominica flag"],"u":"1f1e9-1f1f2"},{"n":["flag-do","dominican republic flag"],"u":"1f1e9-1f1f4"},{"n":["flag-dz","algeria flag"],"u":"1f1e9-1f1ff"},{"n":["flag-ea","ceuta & melilla flag"],"u":"1f1ea-1f1e6"},{"n":["flag-ec","ecuador flag"],"u":"1f1ea-1f1e8"},{"n":["flag-ee","estonia flag"],"u":"1f1ea-1f1ea"},{"n":["flag-eg","egypt flag"],"u":"1f1ea-1f1ec"},{"n":["flag-eh","western sahara flag"],"u":"1f1ea-1f1ed"},{"n":["flag-er","eritrea flag"],"u":"1f1ea-1f1f7"},{"n":["es","flag-es","spain flag"],"u":"1f1ea-1f1f8"},{"n":["flag-et","ethiopia flag"],"u":"1f1ea-1f1f9"},{"n":["flag-eu","european union flag"],"u":"1f1ea-1f1fa"},{"n":["flag-fi","finland flag"],"u":"1f1eb-1f1ee"},{"n":["flag-fj","fiji flag"],"u":"1f1eb-1f1ef"},{"n":["flag-fk","falkland islands flag"],"u":"1f1eb-1f1f0"},{"n":["flag-fm","micronesia flag"],"u":"1f1eb-1f1f2"},{"n":["flag-fo","faroe islands flag"],"u":"1f1eb-1f1f4"},{"n":["fr","flag-fr","france flag"],"u":"1f1eb-1f1f7"},{"n":["flag-ga","gabon flag"],"u":"1f1ec-1f1e6"},{"n":["gb","uk","flag-gb","united kingdom flag"],"u":"1f1ec-1f1e7"},{"n":["flag-gd","grenada flag"],"u":"1f1ec-1f1e9"},{"n":["flag-ge","georgia flag"],"u":"1f1ec-1f1ea"},{"n":["flag-gf","french guiana flag"],"u":"1f1ec-1f1eb"},{"n":["flag-gg","guernsey flag"],"u":"1f1ec-1f1ec"},{"n":["flag-gh","ghana flag"],"u":"1f1ec-1f1ed"},{"n":["flag-gi","gibraltar flag"],"u":"1f1ec-1f1ee"},{"n":["flag-gl","greenland flag"],"u":"1f1ec-1f1f1"},{"n":["flag-gm","gambia flag"],"u":"1f1ec-1f1f2"},{"n":["flag-gn","guinea flag"],"u":"1f1ec-1f1f3"},{"n":["flag-gp","guadeloupe flag"],"u":"1f1ec-1f1f5"},{"n":["flag-gq","equatorial guinea flag"],"u":"1f1ec-1f1f6"},{"n":["flag-gr","greece flag"],"u":"1f1ec-1f1f7"},{"n":["flag-gs","south georgia & south sandwich islands flag"],"u":"1f1ec-1f1f8"},{"n":["flag-gt","guatemala flag"],"u":"1f1ec-1f1f9"},{"n":["flag-gu","guam flag"],"u":"1f1ec-1f1fa"},{"n":["flag-gw","guinea-bissau flag"],"u":"1f1ec-1f1fc"},{"n":["flag-gy","guyana flag"],"u":"1f1ec-1f1fe"},{"n":["flag-hk","hong kong sar china flag"],"u":"1f1ed-1f1f0"},{"n":["flag-hm","heard & mcdonald islands flag"],"u":"1f1ed-1f1f2"},{"n":["flag-hn","honduras flag"],"u":"1f1ed-1f1f3"},{"n":["flag-hr","croatia flag"],"u":"1f1ed-1f1f7"},{"n":["flag-ht","haiti flag"],"u":"1f1ed-1f1f9"},{"n":["flag-hu","hungary flag"],"u":"1f1ed-1f1fa"},{"n":["flag-ic","canary islands flag"],"u":"1f1ee-1f1e8"},{"n":["flag-id","indonesia flag"],"u":"1f1ee-1f1e9"},{"n":["flag-ie","ireland flag"],"u":"1f1ee-1f1ea"},{"n":["flag-il","israel flag"],"u":"1f1ee-1f1f1"},{"n":["flag-im","isle of man flag"],"u":"1f1ee-1f1f2"},{"n":["flag-in","india flag"],"u":"1f1ee-1f1f3"},{"n":["flag-io","british indian ocean territory flag"],"u":"1f1ee-1f1f4"},{"n":["flag-iq","iraq flag"],"u":"1f1ee-1f1f6"},{"n":["flag-ir","iran flag"],"u":"1f1ee-1f1f7"},{"n":["flag-is","iceland flag"],"u":"1f1ee-1f1f8"},{"n":["it","flag-it","italy flag"],"u":"1f1ee-1f1f9"},{"n":["flag-je","jersey flag"],"u":"1f1ef-1f1ea"},{"n":["flag-jm","jamaica flag"],"u":"1f1ef-1f1f2"},{"n":["flag-jo","jordan flag"],"u":"1f1ef-1f1f4"},{"n":["jp","flag-jp","japan flag"],"u":"1f1ef-1f1f5"},{"n":["flag-ke","kenya flag"],"u":"1f1f0-1f1ea"},{"n":["flag-kg","kyrgyzstan flag"],"u":"1f1f0-1f1ec"},{"n":["flag-kh","cambodia flag"],"u":"1f1f0-1f1ed"},{"n":["flag-ki","kiribati flag"],"u":"1f1f0-1f1ee"},{"n":["flag-km","comoros flag"],"u":"1f1f0-1f1f2"},{"n":["flag-kn","st. kitts & nevis flag"],"u":"1f1f0-1f1f3"},{"n":["flag-kp","north korea flag"],"u":"1f1f0-1f1f5"},{"n":["kr","flag-kr","south korea flag"],"u":"1f1f0-1f1f7"},{"n":["flag-kw","kuwait flag"],"u":"1f1f0-1f1fc"},{"n":["flag-ky","cayman islands flag"],"u":"1f1f0-1f1fe"},{"n":["flag-kz","kazakhstan flag"],"u":"1f1f0-1f1ff"},{"n":["flag-la","laos flag"],"u":"1f1f1-1f1e6"},{"n":["flag-lb","lebanon flag"],"u":"1f1f1-1f1e7"},{"n":["flag-lc","st. lucia flag"],"u":"1f1f1-1f1e8"},{"n":["flag-li","liechtenstein flag"],"u":"1f1f1-1f1ee"},{"n":["flag-lk","sri lanka flag"],"u":"1f1f1-1f1f0"},{"n":["flag-lr","liberia flag"],"u":"1f1f1-1f1f7"},{"n":["flag-ls","lesotho flag"],"u":"1f1f1-1f1f8"},{"n":["flag-lt","lithuania flag"],"u":"1f1f1-1f1f9"},{"n":["flag-lu","luxembourg flag"],"u":"1f1f1-1f1fa"},{"n":["flag-lv","latvia flag"],"u":"1f1f1-1f1fb"},{"n":["flag-ly","libya flag"],"u":"1f1f1-1f1fe"},{"n":["flag-ma","morocco flag"],"u":"1f1f2-1f1e6"},{"n":["flag-mc","monaco flag"],"u":"1f1f2-1f1e8"},{"n":["flag-md","moldova flag"],"u":"1f1f2-1f1e9"},{"n":["flag-me","montenegro flag"],"u":"1f1f2-1f1ea"},{"n":["flag-mf","st. martin flag"],"u":"1f1f2-1f1eb"},{"n":["flag-mg","madagascar flag"],"u":"1f1f2-1f1ec"},{"n":["flag-mh","marshall islands flag"],"u":"1f1f2-1f1ed"},{"n":["flag-mk","north macedonia flag"],"u":"1f1f2-1f1f0"},{"n":["flag-ml","mali flag"],"u":"1f1f2-1f1f1"},{"n":["flag-mm","myanmar (burma) flag"],"u":"1f1f2-1f1f2"},{"n":["flag-mn","mongolia flag"],"u":"1f1f2-1f1f3"},{"n":["flag-mo","macao sar china flag"],"u":"1f1f2-1f1f4"},{"n":["flag-mp","northern mariana islands flag"],"u":"1f1f2-1f1f5"},{"n":["flag-mq","martinique flag"],"u":"1f1f2-1f1f6"},{"n":["flag-mr","mauritania flag"],"u":"1f1f2-1f1f7"},{"n":["flag-ms","montserrat flag"],"u":"1f1f2-1f1f8"},{"n":["flag-mt","malta flag"],"u":"1f1f2-1f1f9"},{"n":["flag-mu","mauritius flag"],"u":"1f1f2-1f1fa"},{"n":["flag-mv","maldives flag"],"u":"1f1f2-1f1fb"},{"n":["flag-mw","malawi flag"],"u":"1f1f2-1f1fc"},{"n":["flag-mx","mexico flag"],"u":"1f1f2-1f1fd"},{"n":["flag-my","malaysia flag"],"u":"1f1f2-1f1fe"},{"n":["flag-mz","mozambique flag"],"u":"1f1f2-1f1ff"},{"n":["flag-na","namibia flag"],"u":"1f1f3-1f1e6"},{"n":["flag-nc","new caledonia flag"],"u":"1f1f3-1f1e8"},{"n":["flag-ne","niger flag"],"u":"1f1f3-1f1ea"},{"n":["flag-nf","norfolk island flag"],"u":"1f1f3-1f1eb"},{"n":["flag-ng","nigeria flag"],"u":"1f1f3-1f1ec"},{"n":["flag-ni","nicaragua flag"],"u":"1f1f3-1f1ee"},{"n":["flag-nl","netherlands flag"],"u":"1f1f3-1f1f1"},{"n":["flag-no","norway flag"],"u":"1f1f3-1f1f4"},{"n":["flag-np","nepal flag"],"u":"1f1f3-1f1f5"},{"n":["flag-nr","nauru flag"],"u":"1f1f3-1f1f7"},{"n":["flag-nu","niue flag"],"u":"1f1f3-1f1fa"},{"n":["flag-nz","new zealand flag"],"u":"1f1f3-1f1ff"},{"n":["flag-om","oman flag"],"u":"1f1f4-1f1f2"},{"n":["flag-pa","panama flag"],"u":"1f1f5-1f1e6"},{"n":["flag-pe","peru flag"],"u":"1f1f5-1f1ea"},{"n":["flag-pf","french polynesia flag"],"u":"1f1f5-1f1eb"},{"n":["flag-pg","papua new guinea flag"],"u":"1f1f5-1f1ec"},{"n":["flag-ph","philippines flag"],"u":"1f1f5-1f1ed"},{"n":["flag-pk","pakistan flag"],"u":"1f1f5-1f1f0"},{"n":["flag-pl","poland flag"],"u":"1f1f5-1f1f1"},{"n":["flag-pm","st. pierre & miquelon flag"],"u":"1f1f5-1f1f2"},{"n":["flag-pn","pitcairn islands flag"],"u":"1f1f5-1f1f3"},{"n":["flag-pr","puerto rico flag"],"u":"1f1f5-1f1f7"},{"n":["flag-ps","palestinian territories flag"],"u":"1f1f5-1f1f8"},{"n":["flag-pt","portugal flag"],"u":"1f1f5-1f1f9"},{"n":["flag-pw","palau flag"],"u":"1f1f5-1f1fc"},{"n":["flag-py","paraguay flag"],"u":"1f1f5-1f1fe"},{"n":["flag-qa","qatar flag"],"u":"1f1f6-1f1e6"},{"n":["flag-re","réunion flag"],"u":"1f1f7-1f1ea"},{"n":["flag-ro","romania flag"],"u":"1f1f7-1f1f4"},{"n":["flag-rs","serbia flag"],"u":"1f1f7-1f1f8"},{"n":["ru","flag-ru","russia flag"],"u":"1f1f7-1f1fa"},{"n":["flag-rw","rwanda flag"],"u":"1f1f7-1f1fc"},{"n":["flag-sa","saudi arabia flag"],"u":"1f1f8-1f1e6"},{"n":["flag-sb","solomon islands flag"],"u":"1f1f8-1f1e7"},{"n":["flag-sc","seychelles flag"],"u":"1f1f8-1f1e8"},{"n":["flag-sd","sudan flag"],"u":"1f1f8-1f1e9"},{"n":["flag-se","sweden flag"],"u":"1f1f8-1f1ea"},{"n":["flag-sg","singapore flag"],"u":"1f1f8-1f1ec"},{"n":["flag-sh","st. helena flag"],"u":"1f1f8-1f1ed"},{"n":["flag-si","slovenia flag"],"u":"1f1f8-1f1ee"},{"n":["flag-sj","svalbard & jan mayen flag"],"u":"1f1f8-1f1ef"},{"n":["flag-sk","slovakia flag"],"u":"1f1f8-1f1f0"},{"n":["flag-sl","sierra leone flag"],"u":"1f1f8-1f1f1"},{"n":["flag-sm","san marino flag"],"u":"1f1f8-1f1f2"},{"n":["flag-sn","senegal flag"],"u":"1f1f8-1f1f3"},{"n":["flag-so","somalia flag"],"u":"1f1f8-1f1f4"},{"n":["flag-sr","suriname flag"],"u":"1f1f8-1f1f7"},{"n":["flag-ss","south sudan flag"],"u":"1f1f8-1f1f8"},{"n":["flag-st","são tomé & prÃncipe flag"],"u":"1f1f8-1f1f9"},{"n":["flag-sv","el salvador flag"],"u":"1f1f8-1f1fb"},{"n":["flag-sx","sint maarten flag"],"u":"1f1f8-1f1fd"},{"n":["flag-sy","syria flag"],"u":"1f1f8-1f1fe"},{"n":["flag-sz","eswatini flag"],"u":"1f1f8-1f1ff"},{"n":["flag-ta","tristan da cunha flag"],"u":"1f1f9-1f1e6"},{"n":["flag-tc","turks & caicos islands flag"],"u":"1f1f9-1f1e8"},{"n":["flag-td","chad flag"],"u":"1f1f9-1f1e9"},{"n":["flag-tf","french southern territories flag"],"u":"1f1f9-1f1eb"},{"n":["flag-tg","togo flag"],"u":"1f1f9-1f1ec"},{"n":["flag-th","thailand flag"],"u":"1f1f9-1f1ed"},{"n":["flag-tj","tajikistan flag"],"u":"1f1f9-1f1ef"},{"n":["flag-tk","tokelau flag"],"u":"1f1f9-1f1f0"},{"n":["flag-tl","timor-leste flag"],"u":"1f1f9-1f1f1"},{"n":["flag-tm","turkmenistan flag"],"u":"1f1f9-1f1f2"},{"n":["flag-tn","tunisia flag"],"u":"1f1f9-1f1f3"},{"n":["flag-to","tonga flag"],"u":"1f1f9-1f1f4"},{"n":["flag-tr","turkey flag"],"u":"1f1f9-1f1f7"},{"n":["flag-tt","trinidad & tobago flag"],"u":"1f1f9-1f1f9"},{"n":["flag-tv","tuvalu flag"],"u":"1f1f9-1f1fb"},{"n":["flag-tw","taiwan flag"],"u":"1f1f9-1f1fc"},{"n":["flag-tz","tanzania flag"],"u":"1f1f9-1f1ff"},{"n":["flag-ua","ukraine flag"],"u":"1f1fa-1f1e6"},{"n":["flag-ug","uganda flag"],"u":"1f1fa-1f1ec"},{"n":["flag-um","u.s. outlying islands flag"],"u":"1f1fa-1f1f2"},{"n":["flag-un","united nations flag"],"u":"1f1fa-1f1f3"},{"n":["us","flag-us","united states flag"],"u":"1f1fa-1f1f8"},{"n":["flag-uy","uruguay flag"],"u":"1f1fa-1f1fe"},{"n":["flag-uz","uzbekistan flag"],"u":"1f1fa-1f1ff"},{"n":["flag-va","vatican city flag"],"u":"1f1fb-1f1e6"},{"n":["flag-vc","st. vincent & grenadines flag"],"u":"1f1fb-1f1e8"},{"n":["flag-ve","venezuela flag"],"u":"1f1fb-1f1ea"},{"n":["flag-vg","british virgin islands flag"],"u":"1f1fb-1f1ec"},{"n":["flag-vi","u.s. virgin islands flag"],"u":"1f1fb-1f1ee"},{"n":["flag-vn","vietnam flag"],"u":"1f1fb-1f1f3"},{"n":["flag-vu","vanuatu flag"],"u":"1f1fb-1f1fa"},{"n":["flag-wf","wallis & futuna flag"],"u":"1f1fc-1f1eb"},{"n":["flag-ws","samoa flag"],"u":"1f1fc-1f1f8"},{"n":["flag-xk","kosovo flag"],"u":"1f1fd-1f1f0"},{"n":["flag-ye","yemen flag"],"u":"1f1fe-1f1ea"},{"n":["flag-yt","mayotte flag"],"u":"1f1fe-1f1f9"},{"n":["flag-za","south africa flag"],"u":"1f1ff-1f1e6"},{"n":["flag-zm","zambia flag"],"u":"1f1ff-1f1f2"},{"n":["flag-zw","zimbabwe flag"],"u":"1f1ff-1f1fc"},{"n":["england flag","flag-england"],"u":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f"},{"n":["scotland flag","flag-scotland"],"u":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f"},{"n":["wales flag","flag-wales"],"u":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f"}]} as EmojiData
\ No newline at end of file
diff --git a/src/components/StickerMenu/emojis/index.ts b/src/components/StickerMenu/emojis/index.ts
new file mode 100644
index 0000000..49b18c2
--- /dev/null
+++ b/src/components/StickerMenu/emojis/index.ts
@@ -0,0 +1,14 @@
+
+export type EmojiData = Record
+
+
+export type Emoji = {
+ label: string,
+ desc: string,
+ unicode: string,
+ variants?: string[]
+}
\ No newline at end of file
diff --git a/src/css/FileTree.css b/src/css/FileTree.css
new file mode 100644
index 0000000..3ecd8ab
--- /dev/null
+++ b/src/css/FileTree.css
@@ -0,0 +1,82 @@
+
+.mk-hide-ribbon.mod-macos.is-hidden-frameless:not(.is-fullscreen):not(.is-popout-window):not(.is-mobile) .workspace-tabs.mod-top-left-space .workspace-tab-header-container {
+ padding-left: calc(var(--frame-left-space) + var(--ribbon-width));
+}
+
+.mk-hide-ribbon:not(.is-mobile) .workspace-ribbon.mod-left {
+ display:none;
+}
+.mk-hide-ribbon:not(.is-mobile) .workspace-ribbon.mod-right {
+ visibility:hidden;
+ position:absolute;
+}
+.mk-hide-ribbon:not(.is-mobile) .workspace-split.mod-right-split {
+ margin-right:0;
+}
+.mk-hide-tabs .mod-left-split .mod-top-left-space .workspace-tab-header-container-inner {
+ visibility: hidden;
+}
+
+.mk-hide-ribbon.is-mobile .workspace-drawer.mod-left .workspace-drawer-inner{
+ padding-left: 0 !important;
+}
+
+.is-mobile .workspace-drawer.mod-left .workspace-drawer-inner .workspace-drawer-header {
+ padding-left: 0 !important;
+}
+
+.mk-hide-ribbon.is-mobile .workspace-drawer.mod-left .workspace-drawer-ribbon {
+ display: none;
+}
+
+.is-mobile .workspace-drawer.mod-left .workspace-drawer-active-tab-header {
+ display:none;
+}
+.is-mobile .workspace-drawer.mod-left .workspace-drawer-header-left {
+ /* display: none; */
+}
+.is-mobile .mk-sidebar button:not(.clickable-icon)
+{
+ padding: unset;
+}
+
+.is-mobile .mk-sidebar .mk-file-icon button
+{
+ font-size: 16px;
+}
+
+body.is-mobile .sidebar-toggle-button {
+ display:flex !important;
+}
+
+.is-mobile .workspace-drawer.mod-left .workspace-drawer-header-icon {
+ position: absolute;
+ right: 20px;
+ top:12px;
+ z-index: 100;
+}
+
+.is-phone .workspace-drawer.mod-left .workspace-drawer-header-icon {
+ top:20px;
+}
+
+.is-mobile .workspace-drawer.mod-left {
+ border-top-right-radius:0;
+ border-bottom-right-radius: 0;
+}
+.mk-sidebar {
+ display:flex;
+ flex-direction:column;
+ height:100%;
+}
+
+.is-mobile .mk-sidebar {
+ flex-direction: column-reverse;
+}
+
+.mk-file-tree {
+ flex:1;
+ overflow: hidden;
+ /* -webkit-overflow-scrolling: touch; */
+
+}
diff --git a/src/css/FlowComponent.css b/src/css/FlowComponent.css
new file mode 100644
index 0000000..1eced82
--- /dev/null
+++ b/src/css/FlowComponent.css
@@ -0,0 +1,140 @@
+.mk-folder-scroller {
+ display: flex !important;
+ align-items: flex-start !important;
+ line-height: 1.4;
+ height: 100%;
+ overflow-x: auto;
+ position: relative;
+ z-index: 0;
+ padding: var(--file-margins);
+}
+.mk-folder-header {
+ display: flex;
+}
+.mk-folder-header .inline-title{
+ flex-grow:1
+}
+.mk-folder-sizer {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ width: 100%;
+ max-width: var(--file-line-width);
+ margin-left: auto;
+ margin-right: auto;
+}
+.mk-file-table-header {
+ margin-top: 24px;
+ color: var(--text-faint)
+}
+.mk-file-table {
+ border-collapse: collapse;
+ table-layout: fixed;
+ width: 100%;
+}
+.mk-file-row {
+
+}
+.mk-file-row:hover {
+ background: var(--background-modifier-hover) !important;
+}
+
+.mk-file-table tr:nth-child(even) {
+ background: var(--color-base-10);
+ }
+.mk-file-row td {
+ padding: 10px 10px;
+
+}
+.mk-file-row .mk-column-file {
+ width: 99%;
+}
+.mk-file-row p {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: var(--font-ui-smaller);
+ color: var(--text-faint);
+ margin: 0;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+.mk-file-row .mk-file-name {
+ font-weight: var(--font-medium);
+}
+
+.mk-file-date {
+ font-size: var(--font-ui-smaller);
+ color: var(--text-muted);
+ width: 100px;
+}
+.mk-column-icon {
+ width: 40px;
+}
+.mk-column-icon svg {
+ width: 16px;
+ height: 16px;
+ color: var(--text-muted)
+}
+
+.mk-flowspace-title svg {
+ width: 16px;
+ height: 16px;
+ color: var(--text-muted)
+}
+
+.mk-flowspace-title p {
+ margin: 0;
+ padding: 0;
+ margin-left: 8px;
+}
+
+.mk-flowspace-title .mk-flowspace-date {
+ font-size: var(--font-ui-smaller);
+ color: var(--text-muted)
+}
+
+.mk-flowspace-title {
+ display:flex;
+ align-items: center;
+ padding: 8px 12px;
+ border-top: 1px solid var(--divider-color);
+
+}
+.mk-flowspace-editor {
+ padding: 0px 12px;
+}
+.mk-flowspace-editor .mk-floweditor {
+ border-top: 1px solid var(--divider-color);
+ padding: 12px 0px;
+}
+.mk-flowspace-container {
+}
+
+.mk-flowspace-title span {
+ flex-grow: 1;
+}
+
+.mk-flowspace-title button {
+ padding: 8px;
+ margin-left: 8px;
+ width: unset;
+}
+
+.mk-flowspace-title button.mk-open {
+ background: var(--icon-color-active);
+}
+
+.mk-flowspace-title:hover {
+ background: var(--color-base-10)
+}
+
+.mk-folder-empty {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--font-ui-small);
+ font-style: italic;
+}
\ No newline at end of file
diff --git a/src/css/FlowEditor.css b/src/css/FlowEditor.css
new file mode 100644
index 0000000..35c1629
--- /dev/null
+++ b/src/css/FlowEditor.css
@@ -0,0 +1,229 @@
+.mk-floweditor .workspace-leaf {
+ all: unset
+}
+.mk-floweditor.hover-editor .popover-content {
+ margin: 0;
+ border-radius: var(--he-popover-border-radius);
+ overflow: hidden;
+ height: 100%;
+ }
+ .mk-floweditor.hover-editor .workspace-leaf,
+.mk-floweditor.hover-editor .workspace-split {
+ height: 100%;
+ width: 100%;
+}
+
+.mk-floweditor .markdown-source-view.mod-cm6 .cm-editor {
+ min-height:auto;
+}
+
+.mk-floweditor .cm-content {
+ padding: 0 !important;
+}
+
+/* FIXES THEMES LIKE ATOM */
+.markdown-source-view.mod-cm6 .cm-content > .internal-embed {
+ contain: unset !important;
+}
+.mk-toggle-on {
+ color: var(--interactive-accent);
+}
+
+.mk-floweditor .view-content {
+ background: none !important;
+}
+
+.mk-floweditor .cm-scroller {
+ padding: 0 !important;
+}
+
+.mk-floweditor-placeholder {
+ border: thin solid var(--divider-color);
+}
+.mk-floweditor .mk-floweditor-title-container {
+ display: flex;
+}
+
+.mk-hidden {
+ display: none !important;
+}
+
+.mk-floweditor-title {
+ padding: 8px 0px;
+ margin: 0;
+ margin-top: 8px;
+ border-top: 1px solid var(--background-modifier-hover);
+ width:100%;
+ display: flex;
+}
+.mk-floweditor-title:hover {
+
+ background: var(--background-modifier-hover)
+}
+.mk-floweditor-title div:not(.collapse) svg
+{
+ transform: rotate(0deg);
+}
+
+.mk-floweditor-title .collapse svg
+{
+ transform: rotate(90deg);
+}
+
+.mk-floweditor-title svg {
+
+margin-left:4px;
+ width: 10px;
+ height: 10px;
+}
+ .mk-flow-hover {
+ margin-top: -34px;
+ }
+
+ .mk-flow-hover > div {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+
+ }
+
+ .mk-flow-seamless .mk-floweditor-container:not(.mk-floweditor-fix) > .mk-floweditor {
+ padding: 8px;
+ border-radius: 4px;
+ border: thin solid var(--divider-color);
+}
+
+.mk-flow-seamless .markdown-embed-title {
+ display:none;
+}
+.mk-flow-seamless .mk-flow-titlebar {
+ display:none;
+}
+
+.mk-flow-classic .mk-flow-titlebar {
+ font-weight: var(--bold-weight);
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 0 0 var(--size-4-2) 0;
+}
+
+.mk-flow-classic .mk-flow-titlebar:empty {
+ margin-bottom: 0 !important;
+ padding: 0 !important;
+}
+
+.mk-flow-classic .mk-floweditor-container:not(.mk-floweditor-fix) > .mk-floweditor {
+ font-style: var(--embed-font-style);
+ background-color: var(--embed-background);
+ border-top: var(--embed-border-top);
+ border-right: var(--embed-border-right);
+ border-bottom: var(--embed-border-bottom);
+ border-left: var(--embed-border-left);
+ padding: var(--embed-padding);
+}
+
+.mk-flow-seamless .markdown-embed {
+ padding: 8px;
+ border-radius: 4px;
+ border: thin solid var(--color-base-20);
+ margin-top: 4px;
+}
+
+.mk-flow-classic .markdown-embed {
+ margin-top: 24px;
+}
+
+.markdown-embed .markdown-rendered h1,
+.markdown-embed .markdown-rendered h2,
+.markdown-embed .markdown-rendered h3,
+.markdown-embed .markdown-rendered h4,
+.markdown-embed .markdown-rendered h5,
+.markdown-embed .markdown-rendered h6 {
+ margin:0;
+}
+
+.markdown-embed p {
+ margin-bottom: 24px;
+}
+
+.mk-flow-seamless .markdown-embed {
+ margin-top: 24px;
+}
+
+.mk-floweditor-container:not(.mk-floweditor-fix) > .mk-floweditor:hover {
+
+}
+
+.mk-floweditor-container {
+ min-height: var(--flow-height);
+}
+
+.mk-floweditor-container > .mk-floweditor:hover {
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.06),
+ 0px 3.4px 6.7px rgba(0, 0, 0, 0.06),
+ 0px 15px 30px rgba(0, 0, 0, 0.15);
+}
+
+.mk-floweditor-container {
+
+ display:inline;
+}
+
+.cm-tooltip-hover {
+ margin-bottom: 30px;
+}
+
+.mk-floweditor-fix > .mk-floweditor {
+ margin-top:-28px;
+}
+
+.cm-tooltip {
+ border: none !important;
+ z-index: var(--layer-popover) !important;
+}
+
+.cm-line:hover > .mk-floweditor-selector {
+ visibility: visible;
+}
+
+.markdown-embed:hover .mk-floweditor-selector {
+ visibility: visible;
+}
+
+.mk-flow-classic .markdown-embed > .mk-floweditor-selector {
+ top: 5px !important;
+}
+
+.mk-flow-seamless .markdown-embed > .mk-floweditor-selector {
+ top: -22px !important;
+ right:4px;
+}
+.mk-flow-seamless .cm-line > .mk-floweditor-selector {
+ top: 3px !important;
+}
+
+.mk-floweditor-selector {
+ position: absolute;
+ visibility: hidden;
+ top: 30px;
+ right:5px;
+ z-index: var(--layer-popover);
+ padding-right:8px;
+ height: 30px;
+ display: flex;
+ gap: 6px;
+}
+
+
+
+.mk-floweditor-selector > div{
+ padding: 4px 6px;
+ border-radius: 4px;
+ background: var(--background-primary);
+ border: thin solid var(--background-modifier-border-hover);
+}
+
+.mk-floweditor-selector > div:hover{
+ background: var(--background-secondary-alt)
+}
\ No newline at end of file
diff --git a/src/css/FolderTreeView.css b/src/css/FolderTreeView.css
new file mode 100644
index 0000000..c433601
--- /dev/null
+++ b/src/css/FolderTreeView.css
@@ -0,0 +1,204 @@
+.mk-tree-wrapper {
+ box-sizing: border-box;
+
+ margin-bottom: 1px;
+ display:flex;
+ align-items:center;
+ padding-right: 12px;
+ position: relative;
+}
+
+.is-mobile .mk-tree-wrapper {
+ padding-top: 6px;
+ padding-bottom: 6px;
+}
+
+.mk-tree-wrapper > div {
+ display:flex;
+ align-items:center;
+ width:100%;
+}
+
+.mk-tree-wrapper > .mk-indicator-bottom::after {
+ content: " ";
+ display: block;
+ position: absolute;
+ height: 2px;
+ border-radius:1px;
+ background: var(--interactive-accent);
+ width: calc(100% - var(--spacing));
+ left: var(--spacing);
+ top: 100%;
+}
+
+.mk-tree-wrapper > .mk-indicator-top::before {
+ content: " ";
+ display: block;
+ position: absolute;
+ height: 2px;
+ border-radius:1px;
+ background: var(--interactive-accent);
+ width: calc(100% - var(--spacing));
+ left: var(--spacing);
+ top: 0%;
+}
+
+.mk-tree-wrapper .mk-indicator-row {
+ background: #dde8f6;
+}
+
+.mk-tree-wrapper.mk-clone {
+ display: inline-block;
+ pointer-events: none;
+ padding: 0;
+ padding-left: 10px;
+ padding-top: 5px;
+}
+.mk-tree-wrapper.mk-clone.mk-tree-item {
+ --vertical-padding: 5px;
+ padding-right: 24px;
+ box-shadow: 0px 15px 15px 0 rgba(34, 33, 81, 0.1);
+}
+
+ .mk-tree-wrapper.mk-ghost{
+ opacity: 0.5;
+ }
+
+
+
+.mk-tree-item {
+ margin-left: var(--spacing);
+ --vertical-padding: 2px;
+ flex-grow: 1;
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: var(--vertical-padding) 2px;
+ box-sizing: border-box;
+ min-width:0;
+ margin-right:4px;
+ height: 28px;
+}
+
+
+.mk-tree-wrapper button {
+background: none;
+border: 0;
+box-shadow: none;
+margin: 0;
+height:24px;
+width: 24px;
+padding: 0
+}
+
+.is-mobile .mk-tree-wrapper .mk-folder-buttons button {
+ margin-left: 8px;
+}
+
+body:not(.is-mobile) .mk-tree-wrapper button:hover {
+background: var(--nav-item-background-hover);
+}
+
+.mk-file-icon {
+ width: 24px;
+ height: 24px;
+}
+
+
+.is-mobile .mk-file-icon {
+ width: unset;
+}
+
+
+
+body:not(.is-mobile) .mk-tree-wrapper .mk-folder-buttons {
+ display:none;
+}
+body:not(.is-mobile) .mk-tree-wrapper:hover .mk-folder-buttons {
+ display:flex;
+
+}
+
+.mk-tree-wrapper svg {
+ width:16px;
+ height:16px;
+ color: var(--text-muted);
+}
+
+.is-mobile .mk-tree-wrapper svg {
+ width: 20px;
+ height: 20px;
+ color: var(--text-faint)
+}
+
+.is-mobile .mk-tree-wrapper .mk-file-icon svg {
+ width: 18px;
+ height: 18px;
+ color: var(--text-faint)
+}
+
+.mk-tree-text {
+ padding: 0.15rem 4px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: var(--font-ui-small);
+ flex-grow: 1;
+}
+
+
+.mk-disable-interaction {
+pointer-events: none;
+}
+
+.mk-disable-selection,
+.mk-clone
+.mk-tree-text {
+ user-select: none;
+ -webkit-user-select: none;
+}
+.view-content {
+padding: 0 !important;
+}
+.mk-collapse svg {
+transform: rotate(90deg);
+ transition: transform 250ms ease;
+ width:14px;
+ height:14px;
+ margin: 5px;
+}
+.mk-collapse.mk-collapsed svg {
+ transform: rotate(0deg);
+}
+
+.mk-is-active:not(.clone) {
+ color: var(--nav-item-color-active);
+
+ background: var(--nav-item-background-active);
+}
+
+body:not(.is-mobile) .mk-tree-wrapper:not(.mk-section-wrapper):not(.mk-disable-interaction):hover {
+ background: var(--nav-item-background-hover);
+ }
+
+ /* .is-mobile .mk-tree-wrapper:not(.mk-section-wrapper):not(.mk-disable-interaction):active {
+ background: var(--nav-item-background-hover);
+ } */
+
+
+.mk-icon-menu {
+ transform: translate3d(-500px, 0px, 0px);
+ z-index: var(--layer-menu)
+}
+.mk-icon-menu .menu {
+ position: static !important;
+ padding: 0 !important;
+ }
+
+.mk-tree-empty {
+ padding-left: var(--spacing);
+ padding-top: 4px;
+ padding-bottom: 4px;
+ font-size: var(--font-ui-small);
+ color: var(--text-faint)
+}
\ No newline at end of file
diff --git a/src/css/InlineMenu.css b/src/css/InlineMenu.css
new file mode 100644
index 0000000..10004cd
--- /dev/null
+++ b/src/css/InlineMenu.css
@@ -0,0 +1,68 @@
+body:not(.is-mobile) .mk-style-menu {
+ margin-left: -80px;
+}
+
+
+
+.mk-style-menu {
+ display: flex;
+ padding: 0;
+ margin-top: -44px;
+}
+
+.is-mobile .mk-style-menu svg {
+ width: 32px;
+ height: 32px;
+}
+
+.is-mobile .mk-style-menu {
+ --mobile-toolbar-height: 48px;
+ border-radius: 0;
+ width:100%;
+ margin-top: 0;
+ overflow-x: auto;
+ justify-content: center;
+ height: var(--mobile-toolbar-height);
+ border-top: var(--divider-width) solid var(--divider-color);
+}
+
+.mk-style-menu .mk-mark {
+ margin: 4px;
+ padding: 4px;
+ border-radius: 4px;
+ display:flex;
+}
+
+.mk-style-menu .mk-mark:hover {
+ background: var(--background-modifier-hover);
+}
+
+.mk-style-menu .mk-mark-active {
+ background: var(--background-modifier-hover);
+}
+
+.mk-style-menu svg {
+ color: var(--text-muted)
+}
+
+.mk-divider {
+ border-left: thin solid var(--background-modifier-hover);
+ width: 1px;
+}
+
+.mk-color {
+ width: 24px;
+ height: 24px;
+ border-radius: 12px;
+ margin: 8px;
+}
+.mk-color:hover {
+ opacity: 0.8
+}
+
+mark {
+ color: unset;
+ border-radius: 2px;
+ margin: 0px 2px;
+ padding: 0px 2px;
+}
\ No newline at end of file
diff --git a/src/css/MainMenu.css b/src/css/MainMenu.css
new file mode 100644
index 0000000..9a0b0cc
--- /dev/null
+++ b/src/css/MainMenu.css
@@ -0,0 +1,56 @@
+.mk-main-menu-container {
+ display: flex;
+ padding: 12px 8px;
+}
+.mk-main-menu-button {
+ text-align:left;
+ padding:8px 8px;
+ border-radius: 4px;
+ align-items: center;
+ display:flex;
+ width:calc(100% - 50px);
+}
+.is-mobile .mk-main-menu-button {
+ font-size: var(--font-ui-medium);
+ font-weight: var(--font-medium);
+}
+.mk-main-menu-button > div {
+ display: flex;
+}
+
+.mk-main-menu-button svg {
+height: 16px;
+width: 16px;
+}
+body:not(.is-mobile) .mk-main-menu-button:hover {
+ background:var(--nav-item-background-hover);
+}
+.mk-main-menu {
+ position: absolute;
+ left: 8px;
+ z-index:var(--layer-menu);
+ margin-top: 2.25rem;
+ margin-left: 2px;
+ background-color: var(--background-secondary);
+ transform-origin: top left;
+ border-radius: var(--radius-m);
+ border: 1px solid var(--background-modifier-border-hover);
+ box-shadow: var(--shadow-s);
+}
+
+.mk-menu-button {
+ display: flex;
+padding-top: 0.5rem;
+padding-bottom: 0.5rem;
+padding-left: 0.5rem;
+padding-right: 0.5rem;
+font-size: 0.75rem;
+line-height: 1.25rem;
+align-items: center;
+width: 100%;
+border-radius: 0.375rem;
+
+}
+.mk-menu-button:hover {
+background: var(--nav-item-background-hover);
+}
\ No newline at end of file
diff --git a/src/css/MakeMenu.css b/src/css/MakeMenu.css
new file mode 100644
index 0000000..70e411c
--- /dev/null
+++ b/src/css/MakeMenu.css
@@ -0,0 +1,17 @@
+.mk-slash-item {
+ display: flex;
+ align-items: center;
+}
+.mk-slash-icon {
+ display: flex;
+ margin-right: 8px;
+}
+.mk-slash-icon svg {
+ width: 16px;
+ height: 16px;
+}
+.cm-active.cm-placeholder:before {
+ content: attr(data-ph);
+ color: var(--text-faint);
+ position: absolute;
+}
\ No newline at end of file
diff --git a/src/css/NewNote.css b/src/css/NewNote.css
new file mode 100644
index 0000000..cfdaf32
--- /dev/null
+++ b/src/css/NewNote.css
@@ -0,0 +1,41 @@
+.mk-flow-bar {
+display: flex;
+}
+.is-mobile .mk-flow-bar {
+ border-top: thin solid var(--divider-color);
+}
+.mk-new-note {
+flex-grow:1;
+ padding: 8px 12px;
+ font-size: var(--font-ui-smaller);
+ margin:4px 12px;
+ display:flex;
+ align-items: center;
+}
+
+.is-mobile .mk-new-note {
+ margin: 8px 12px;
+}
+
+.mk-new-note p {
+ margin:0;
+ margin-left: 8px;
+ padding:0;
+}
+
+.mk-new-note svg {
+width: 16px;
+height: 16px;
+}
+
+.mk-search {
+ display: flex;
+ padding: 8px 8px;
+ margin:4px 12px;
+ margin-left: 0px;
+ }
+
+.mk-search svg {
+width: 16px;
+height: 16px;
+}
diff --git a/src/css/SectionView.css b/src/css/SectionView.css
new file mode 100644
index 0000000..7a9ab3c
--- /dev/null
+++ b/src/css/SectionView.css
@@ -0,0 +1,48 @@
+
+.mk-section {
+ display: flex;
+ padding: 4px 0px;
+ padding-left: 12px;
+ margin-top: 12px;
+ color: var(--text-muted);
+ font-size: var(--font-ui-smaller);
+ width:100%;
+ justify-content: space-between;
+ }
+
+
+ body:not(.is-mobile) .mk-section .mk-section-title:hover {
+ background: var(--nav-item-background-hover);
+}
+
+.mk-section .mk-section-title {
+ height: 24px;
+border-radius:4px;
+display: flex;
+min-width:0;
+align-items: center;
+}
+
+.mk-section .mk-tree-text {
+ padding: 0.25rem 0rem 0.3rem 0.25rem;
+}
+
+body:not(.is-mobile) .mk-section .mk-collapse
+ {
+ display:none;
+ }
+.mk-section .mk-collapse
+{
+display: flex;
+}
+
+body:not(.is-mobile) .mk-section:hover .mk-collapse {
+ display: flex
+ }
+.mk-section .mk-collapse svg {
+margin-left:4px;
+ width: 10px;
+ height: 10px;
+}
+
+
\ No newline at end of file
diff --git a/src/css/StickerMenu.css b/src/css/StickerMenu.css
new file mode 100644
index 0000000..9c7a1c7
--- /dev/null
+++ b/src/css/StickerMenu.css
@@ -0,0 +1,32 @@
+.mk-sticker-menu .suggestion {
+ width:240px;
+ height: 240px;
+ display: flex;
+ flex-wrap: wrap;
+ align-content: flex-start;
+ flex-direction: row;
+}
+.mk-sticker-modal {
+ display: flex;
+ flex-wrap: wrap;
+}
+.mk-sticker-menu .suggestion-item {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ font-size:20px;
+ align-items: center;
+ padding: 0;
+ text-align:center;
+ justify-content: center;
+}
+.mk-sticker-filter {
+ border: none;
+ background: none;
+ border-bottom: thin solid var(--background-modifier-hover);
+ width: 100%;
+ padding: 8px 12px;
+}
+.mk-sticker-menu .suggestion-item:hover {
+ background: var(--background-modifier-hover)
+}
\ No newline at end of file
diff --git a/src/css/makerMode.css b/src/css/makerMode.css
new file mode 100644
index 0000000..0d6bbd3
--- /dev/null
+++ b/src/css/makerMode.css
@@ -0,0 +1,104 @@
+
+/* fix checkbox margin */
+.markdown-source-view.mod-cm6 .task-list-label .task-list-item-checkbox {
+ margin-bottom: 4px;
+}
+
+
+.mk-mark-sans .cm-s-obsidian span.cm-hmd-escape-backslash,
+.mk-mark-sans .cm-s-obsidian .HyperMD-header:not(.mk-reset) span.cm-formatting-header {
+
+ display: inline;
+ position: absolute;
+ right: 100%;
+ white-space: nowrap;
+ color: transparent;
+}
+
+
+.mk-mark-sans .cm-s-obsidian .HyperMD-header.mk-reset span.cm-formatting-header {
+ color: unset;
+ }
+
+
+.mk-mark-sans .mk-reset .cm-fold-indicator {
+ display: none !important;
+}
+.mk-mark-sans div[class*="HyperMD-header-"].mk-reset {
+
+ font-variant: unset;
+ letter-spacing: unset;
+ line-height: unset;
+ font-size: unset;
+ color: unset;
+ font-weight: unset;
+ font-style: unset;
+ font-family: unset;
+}
+
+.mk-mark-sans .HyperMD-quote-lazy:before {
+ content: none !important;
+}
+
+.mk-mark-sans .cm-s-obsidian span.cm-formatting-quote:not(.cm-hmd-callout) {
+ color: transparent;
+}
+
+.mk-mark-sans .cm-s-obsidian span.cm-hmd-escape-backslash::selection,
+.mk-mark-sans .cm-s-obsidian span.cm-formatting-header::selection {
+
+ background: transparent;
+}
+
+
+
+/* .mk-maker-mode .cm-s-obsidian span.cm-formatting-em,
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-strong,
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-strikethrough,
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-code {
+ display: inline;
+ position: absolute;
+ z-index: -1;
+ white-space: nowrap;
+ color: transparent;
+}
+
+
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-em::selection,
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-strong::selection,
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-strikethrough::selection,
+.mk-maker-mode .cm-s-obsidian span.cm-formatting-code::selection {
+ background: transparent;
+} */
+
+
+
+.mk-flow-replace .mk-new-file {
+ /* color: transparent; */
+ background: var(--color-base-10);
+border-bottom: thin solid #333;
+}
+
+.mk-flow-replace .mk-new-file:hover {
+ /* color: transparent; */
+ background: var(--color-base-10);
+border-bottom: thin solid #333;
+}
+
+
+.mobile-toolbar-options-container {
+ border-top: var(--divider-width) solid var(--divider-color);
+}
+
+.mk-mark-sans .markdown-source-view.mod-cm6 .HyperMD-quote:not(.HyperMD-callout):before,
+.mk-mark-sans .markdown-source-view.mod-cm6 .cm-blockquote-border:before {
+ left: 0;
+ content: "\200b";
+ display: block;
+ width: 1px;
+ border-left: var(--blockquote-border-thickness) solid var(--blockquote-border-color);
+ color: transparent;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+}
\ No newline at end of file
diff --git a/src/dispatch/flowDispatch.ts b/src/dispatch/flowDispatch.ts
new file mode 100644
index 0000000..227bc9b
--- /dev/null
+++ b/src/dispatch/flowDispatch.ts
@@ -0,0 +1,23 @@
+
+import { TFile } from "obsidian";
+import { SpawnPortalEvent, eventTypes, PortalType } from "types/types";
+
+export const createFlowEditorInElement = (id: string, el: HTMLElement, type: PortalType, file?: string, from?: number, to?: number) => {
+ let evt = new CustomEvent(eventTypes.spawnPortal, { detail: { id, el, file, from, to, type } });
+ window.dispatchEvent(evt);
+}
+
+export const focusFlowEditor = (id: string, top: boolean) => {
+ let evt = new CustomEvent(eventTypes.focusPortal, { detail: { id, parent: false, top } });
+ window.dispatchEvent(evt);
+}
+
+export const focusFlowEditorParent = (id: string, top: boolean) => {
+ let evt = new CustomEvent(eventTypes.focusPortal, { detail: { id, parent: true, top } });
+ window.dispatchEvent(evt);
+}
+
+export const openFileFlowEditor = (file: string, source: string) => {
+ let evt = new CustomEvent(eventTypes.openFilePortal, { detail: { file, source} });
+ window.dispatchEvent(evt);
+}
\ No newline at end of file
diff --git a/src/hooks/ForceUpdate.tsx b/src/hooks/ForceUpdate.tsx
new file mode 100644
index 0000000..7f813cc
--- /dev/null
+++ b/src/hooks/ForceUpdate.tsx
@@ -0,0 +1,6 @@
+import React, { useState } from 'react';
+
+export default function useForceUpdate() {
+ const [value, setValue] = useState(0);
+ return () => setValue((value) => value + 1);
+}
diff --git a/src/hooks/useLongPress.tsx b/src/hooks/useLongPress.tsx
new file mode 100644
index 0000000..ca990f3
--- /dev/null
+++ b/src/hooks/useLongPress.tsx
@@ -0,0 +1,6 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+
+export function isMouseEvent(e: React.TouchEvent | React.MouseEvent): e is React.MouseEvent {
+ return e && 'screenX' in e;
+}
\ No newline at end of file
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100644
index 0000000..80bc4f9
--- /dev/null
+++ b/src/i18n.ts
@@ -0,0 +1,188 @@
+class T {
+ lang: string
+
+ all = {
+ en: {
+ commands: {
+ h1: "Heading 1",
+ h2: "Heading 2",
+ h3: "Heading 3",
+ list: 'Bullet List',
+ 'ordered-list': "Numbered List",
+ todo: 'To-do List',
+ quote: 'Quote',
+ divider: 'Divider',
+ note: 'Link to Note',
+ link: 'Web Link',
+ callout: 'Callout',
+ codeblock: 'Code Block',
+ emoji: 'Emoji',
+ image: 'Image',
+ flow: 'Flow Note',
+ tag: 'Tag',
+ makeMenu: 'Make Menu',
+ selectStyle: 'Style',
+ toggleKeyboard: 'Toggle Keyboard'
+
+ },
+ styles: {
+ bold: 'Bold',
+ italics: 'Italics',
+ strikethrough: 'Strikethrough',
+ code: 'Code',
+ link: 'Web Link',
+ blocklink: 'Link to Note',
+ textColor: 'Text Color',
+ highlight: 'Highlight'
+ },
+ commandsSuggest: {
+ noResult: "No result",
+ },
+ commandPalette: {
+ enable: "Enable",
+ disabled: "Disable",
+ },
+ menu: {
+ openFilePane: 'Open in a new pane',
+ rename: 'Rename',
+ moveFile: 'Move file to...',
+ duplicate: 'Make a copy',
+ edit: 'Edit',
+ delete: 'Delete',
+ getHelp: 'Make.md Community',
+ openVault: 'Open Another Vault',
+ obSettings: 'Obsidian Settings',
+ commandPalette: 'Command Palette',
+ backToSpace: 'Back to Spaces',
+ newSpace: 'New Space',
+ collapseAllSections: 'Collapse All Spaces',
+ expandAllSections: 'Expand All Spaces',
+ collapseAllFolders: 'Collapse All Folders',
+ expandAllFolders: 'Expand All Folders',
+ spaceTitle: 'Add/Remove in Space'
+ },
+ buttons: {
+ moreOptions: 'More Options',
+ newNote: 'New Note',
+ changeIcon: 'Change Sticker',
+ rename: 'Change Name',
+ createFolder: 'New Folder',
+ createNote: 'New Note',
+ createSection: 'New Space',
+ cancel: 'Cancel',
+ search: 'Search',
+ toggleFlow: 'Hide Flow',
+ openFlow: 'Open Flow',
+ hideFlow: 'Hide Flow',
+ openLink: 'Open Link'
+ },
+ labels: {
+ createFolder: 'New Folder Name',
+ rename: 'Rename Note',
+ renameSection: 'Rename Space',
+ createSection: 'New Space',
+ createNote: 'New Note Name',
+ collapse: 'Collapse',
+ expand: 'Expand',
+ findStickers: "Find Sticker",
+ placeholder: "Type '/' for commands",
+ noFile: 'is not created yet. Click to create.'
+
+
+ },
+ flowView: {
+ emptyDoc: 'Empty Document',
+ itemsCount: ' Items',
+ emptyFolder: 'This Folder is Empty'
+ },
+ notice: {
+ duplicateFile: 'Folder already contains note with same name',
+ addedToSection: 'Added to Space'
+ },
+ settings: {
+ sectionSidebar: 'Spaces',
+ sectionEditor: 'Maker Mode',
+ sectionFlow: 'Flow Editor',
+ spaces: {
+ name: 'Spaces',
+ desc: `Spaces gives you control over how you organize your files`
+ },
+ spacesStickers: {
+ name: 'Stickers',
+ desc: `Use Emojis to make it easier to find your notes`
+ },
+ spacesDeleteOption: {
+ name: 'Delete File Option',
+ desc: 'Select how you want files to be deleted'
+ },
+ spacesDeleteOptions: {
+ permanant: 'Delete Permanently',
+ trash: 'Move to Obsidian Trash',
+ 'system-trash': 'Move to System Trash'
+ },
+ sidebarRibbon: {
+ name: 'Show Ribbon Bar',
+ desc: `Show/hide Obsidian ribbon bar`
+ },
+ sidebarTabs: {
+ name: 'Show Sidebar Tabs',
+ desc: `Show/hide other sidebar tabs`
+ },
+ spacesPerformance: {
+ name: 'Spaces Performance Mode',
+ desc: `Turn on performance mode for Spaces, may affect scrolling appearance. Requires Restart`
+ },
+ inlineStyler: {
+ name: 'Inline Styler',
+ desc: `Select text to add styling, recommended for Flow Editor`
+ },
+ inlineStylerColor: {
+ name: 'Text and Highlight Colors 🧪',
+ desc: `Select text color and highlight color, (this may change in the future because of the limitations with HTML and Obsidian)`
+ },
+ makeChar: {
+ name: 'Make Menu Trigger',
+ desc: 'Character to open the Make Menu'
+ },
+ mobileMakeBar: {
+ name: 'Make Bar (Mobile)',
+ desc: 'Replaces the mobile toolbar'
+ },
+ editorMarkSans: {
+ name: 'Mark Sans 🧪',
+ desc: `Use the editor without Markdown.`
+ },
+ editorMakePlacholder: {
+ name: 'Make Menu Hint Text',
+ desc: `Show a hint text on how to open the Make Menu Shortcut`
+ },
+ editorMakeMenu: {
+ name: 'Make Menu Shortcut',
+ desc: `Open the Make menu to quickly add content`
+ },
+ editorFlowReplace: {
+ name: 'Flow Editor',
+ desc: `Open your internal links or toggle your embeds in the flow editor.`
+ },
+ editorFlowStyle: {
+ name: 'Flow Editor Style',
+ desc: 'Select a theme for your flow editors',
+ seamless: 'Seamless',
+ classic: 'Classic',
+ }
+
+ }
+ },
+
+ }
+
+ constructor() {
+ this.lang = localStorage.getItem("language")
+ }
+
+ get texts(): typeof this.all.en {
+ return this.all["en"]
+ }
+}
+
+export default new T().texts
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..a147c89
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,213 @@
+import { FileExplorerPlugin, Plugin, addIcon, TAbstractFile, MarkdownView, WorkspaceLeaf, Menu, EphemeralState, ViewState, WorkspaceItem, WorkspaceContainer, Workspace, App, Plugin_2 } from 'obsidian';
+import { FILE_TREE_VIEW_TYPE, FileTreeView, ICON, SETS_VIEW_TYPE } from './components/Spaces/FileTreeView';
+import { MakeMDPluginSettings as MakeMDPluginSettings, MakeMDPluginSettingsTab, DEFAULT_SETTINGS } from './settings';
+import { eventTypes, FocusPortalEvent, OpenFilePortalEvent, SpawnPortalEvent, VaultChange } from 'types/types';
+import MakeMenu from 'components/MakeMenu/MakeMenu';
+import StickerMenu from 'components/StickerMenu/StickerMenu';
+import { FlowView, FOLDER_VIEW_TYPE } from 'components/FlowView/FlowView';
+import { FlowEditor } from 'components/FlowEditor/FlowEditor';
+import { around } from "monkey-around";
+import { EditorView } from '@codemirror/view'
+import 'css/makerMode.css'
+import { cmExtensions } from 'cm-extensions/cmExtensions';
+import { focusPortal, loadFlowEditorsForLeaf, openFileFromPortal, spawnNewPortal } from 'utils/flowEditor';
+import { replaceAllEmbed } from 'utils/markdownPost';
+import { toggleMark } from 'cm-extensions/inlineStylerView/marks';
+import { platformIsMobile } from 'utils/utils';
+import { replaceMobileMainMenu } from 'components/Spaces/MainMenu';
+import { loadStylerIntoContainer } from 'cm-extensions/inlineStylerView/InlineMenu';
+import { patchFileExplorer, patchWorkspace } from 'utils/patches';
+import { platform } from 'os';
+import { getActiveCM } from 'utils/codemirror';
+import { mkLogo } from 'utils/icons';
+export default class MakeMDPlugin extends Plugin {
+ settings: MakeMDPluginSettings;
+ activeEditorView?: MarkdownView;
+ flowEditors: FlowEditor[];
+
+ toggleBold () {
+ const cm = getActiveCM();
+ if (cm) {
+ cm.dispatch({
+ annotations: toggleMark.of('strong')
+ })
+ }
+ }
+ toggleEm () {
+ const cm = getActiveCM();
+ if (cm) {
+ cm.dispatch({
+ annotations: toggleMark.of('em')
+ })
+ }
+ }
+
+ loadSpaces () {
+ patchWorkspace(this);
+ document.body.classList.toggle('mk-hide-ribbon', !this.settings.sidebarRibbon);
+ document.body.classList.toggle('mk-hide-ribbon', !this.settings.sidebarRibbon);
+ document.body.classList.toggle('mk-hide-tabs', !this.settings.sidebarTabs);
+ this.registerView(FOLDER_VIEW_TYPE, (leaf) => {
+ return new FlowView(leaf, this);
+ });
+ if (this.settings.spacesEnabled) {
+ patchFileExplorer(this);
+ this.registerView(FILE_TREE_VIEW_TYPE, (leaf) => {
+ return new FileTreeView(leaf, this);
+ });
+ this.app.workspace.onLayoutReady(async () => {
+ await this.openFileTreeLeaf(true);
+ });
+ }
+
+ this.app.vault.on('create', this.onCreate);
+ this.app.vault.on('delete', this.onDelete);
+ this.app.vault.on('rename', this.onRename);
+ }
+
+ loadFlowEditor () {
+ document.body.classList.toggle('mk-flow-replace', this.settings.editorFlow);
+ document.body.classList.toggle('mk-flow-'+this.settings.editorFlowStyle, true);
+ if (this.settings.editorFlow) {
+ this.registerMarkdownPostProcessor((element, context) => {
+ const removeAllFlowMarks = (el: HTMLElement) => {
+ const embeds = el.querySelectorAll(".internal-embed");
+
+ for (let index = 0; index < embeds.length; index++) {
+ const embed = embeds.item(index);
+ if (embed.previousSibling && embed.previousSibling.textContent.slice(-1) == '!')
+ embed.previousSibling.textContent = embed.previousSibling.textContent.slice(0, -1)
+ }
+ }
+ removeAllFlowMarks(element);
+ replaceAllEmbed(element, context);
+ })
+
+
+ window.addEventListener(eventTypes.spawnPortal, this.spawnPortal);
+ window.addEventListener(eventTypes.focusPortal, this.focusPortal);
+ window.addEventListener(eventTypes.openFilePortal, this.openFileFromPortal);
+ }
+ }
+
+ loadMakerMode () {
+ document.body.classList.toggle('mk-mark-sans', this.settings.markSans);
+ this.addCommand({
+ id: 'mk-toggle-bold',
+ name: 'Toggle Bold',
+ callback: () => this.toggleBold(),
+ hotkeys: [
+ {
+ modifiers: ['Mod'],
+ key: 'b',
+ },
+ ],
+ });
+
+ this.addCommand({
+ id: 'mk-toggle-italics',
+ name: 'Toggle Italics',
+ callback: () => this.toggleEm(),
+ hotkeys: [
+ {
+ modifiers: ['Mod', 'Shift'],
+ key: 'i',
+ },
+ ],
+ });
+ this.registerEditorSuggest(new MakeMenu(this.app, this))
+ this.registerEditorSuggest(new StickerMenu(this.app, this))
+ if (platformIsMobile() && this.settings.mobileMakeBar)
+ loadStylerIntoContainer(app.mobileToolbar.containerEl);
+ }
+ async onload() {
+ window.make = this;
+ addIcon('mk-logo', mkLogo)
+ console.log('Loading Make.md');
+ // Load Settings
+ this.addSettingTab(new MakeMDPluginSettingsTab(this.app, this));
+ await this.loadSettings();
+ this.loadSpaces();
+ this.loadFlowEditor();
+ this.loadMakerMode();
+ this.registerEditorExtension(cmExtensions(this, platformIsMobile()));
+ }
+
+ //Flow Editor Listeners
+ openFileFromPortal (e: OpenFilePortalEvent) {
+ openFileFromPortal(this, e)
+ }
+ spawnPortal (e: SpawnPortalEvent) {
+ spawnNewPortal(this, e);
+ }
+ focusPortal (e: FocusPortalEvent) {
+ focusPortal(this, e);
+ }
+
+ //Spaces Listeners
+ triggerVaultChangeEvent = (file: TAbstractFile, changeType: VaultChange, oldPath?: string) => {
+ let event = new CustomEvent(eventTypes.vaultChange, {
+ detail: {
+ file: file,
+ changeType: changeType,
+ oldPath: oldPath ? oldPath : '',
+ },
+ });
+ window.dispatchEvent(event);
+ };
+ onCreate = (file: TAbstractFile) => this.triggerVaultChangeEvent(file, 'create', '');
+ onDelete = (file: TAbstractFile) => this.triggerVaultChangeEvent(file, 'delete', '');
+ onRename = (file: TAbstractFile, oldPath: string) => this.triggerVaultChangeEvent(file, 'rename', oldPath);
+
+ openFileTreeLeaf = async (showAfterAttach: boolean) => {
+ let leafs = this.app.workspace.getLeavesOfType(FILE_TREE_VIEW_TYPE);
+ if (leafs.length == 0) {
+ let leaf = this.app.workspace.getLeftLeaf(false);
+ await leaf.setViewState({ type: FILE_TREE_VIEW_TYPE });
+ if (showAfterAttach) this.app.workspace.revealLeaf(leaf);
+ } else {
+ leafs.forEach((leaf) => this.app.workspace.revealLeaf(leaf));
+ }
+ replaceMobileMainMenu(this);
+ };
+
+ detachFileTreeLeafs = () => {
+ let leafs = this.app.workspace.getLeavesOfType(FILE_TREE_VIEW_TYPE);
+ for (let leaf of leafs) {
+ if ((leaf.view as FileTreeView).destroy)
+ (leaf.view as FileTreeView).destroy();
+ leaf.detach();
+ }
+ };
+
+ refreshTreeLeafs = () => {
+ this.detachFileTreeLeafs();
+ this.openFileTreeLeaf(true);
+ };
+
+ async loadSettings() {
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
+ }
+
+ async saveSettings(refresh=true) {
+ await this.saveData(this.settings);
+ if(refresh) {
+ let evt = new CustomEvent(eventTypes.settingsChanged, {});
+ window.dispatchEvent(evt);
+ }
+ }
+
+
+
+ onunload() {
+ console.log('Unloading Make.md');
+ window.removeEventListener(eventTypes.spawnPortal, this.spawnPortal)
+ window.removeEventListener(eventTypes.focusPortal, this.focusPortal)
+ window.removeEventListener(eventTypes.openFilePortal, this.openFileFromPortal)
+ this.detachFileTreeLeafs();
+ // Remove event listeners
+ this.app.vault.off('create', this.onCreate);
+ this.app.vault.off('delete', this.onDelete);
+ this.app.vault.off('rename', this.onRename);
+ }
+}
diff --git a/src/recoil/pluginState.ts b/src/recoil/pluginState.ts
new file mode 100644
index 0000000..f5191e6
--- /dev/null
+++ b/src/recoil/pluginState.ts
@@ -0,0 +1,40 @@
+import { TFolder } from 'obsidian';
+import { atom } from 'recoil';
+import { SectionTree } from 'types/types';
+
+export const activeFile = atom({
+ key: 'spacesActiveFile',
+ default: null as string,
+ dangerouslyAllowMutability: true,
+});
+
+
+export const folderTree = atom({
+ key: 'spacesFolderTree',
+ default: null as TFolder,
+ dangerouslyAllowMutability: true,
+});
+
+export const sections = atom({
+ key: 'spacesSections',
+ default: [] as SectionTree[],
+ dangerouslyAllowMutability: true,
+});
+
+
+export const fileIcons = atom({
+ key: 'spacesIcons',
+ default: [] as [string, string][],
+});
+
+export const openFolders = atom({
+ key: 'spacesOpenFolders',
+ default: [] as string[],
+});
+
+
+export const focusedFolder = atom({
+ key: 'spacesFocusedFolder',
+ default: null as TFolder,
+ dangerouslyAllowMutability: true,
+});
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..a80830f
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,264 @@
+import MakeMDPlugin from './main';
+import { PluginSettingTab, Setting, App, DropdownComponent } from 'obsidian';
+import { eventTypes, SectionTree, StringTree } from 'types/types';
+import t from 'i18n'
+export type DeleteFileOption = 'trash' | 'permanent' | 'system-trash';
+
+export interface MakeMDPluginSettings {
+ filePreviewOnHover: boolean;
+ markSans: boolean;
+ makeMenuPlaceholder: boolean;
+ inlineStyler: boolean;
+ mobileMakeBar: boolean;
+ inlineStylerColors: boolean;
+ editorFlow: boolean;
+ editorFlowStyle: string;
+ spacesEnabled: boolean;
+ spacesPerformance: boolean;
+ spacesStickers: boolean;
+ sidebarRibbon: boolean;
+ sidebarTabs: boolean;
+ deleteFileOption: DeleteFileOption;
+ folderRank: StringTree;
+ openFolders: string[];
+ fileIcons: [string, string][];
+ spaces: SectionTree[];
+ vaultCollapsed: boolean;
+ menuTriggerChar: string;
+ emojiTriggerChar: string;
+}
+
+export const DEFAULT_SETTINGS: MakeMDPluginSettings = {
+
+ filePreviewOnHover: false,
+ markSans: true,
+ makeMenuPlaceholder: true,
+ mobileMakeBar: true,
+ inlineStyler: true,
+ inlineStylerColors: false,
+ editorFlow: true,
+ editorFlowStyle: 'seamless',
+ spacesEnabled: true,
+ spacesPerformance: false,
+ spacesStickers: true,
+ sidebarRibbon: false,
+ sidebarTabs: false,
+ deleteFileOption: 'trash',
+ folderRank: {
+ node: 'root',
+ children: [],
+ isFolder: true,
+ },
+ openFolders: [],
+ fileIcons: [],
+ spaces: [],
+ vaultCollapsed: false,
+ menuTriggerChar: '/',
+ emojiTriggerChar: ':'
+};
+
+export class MakeMDPluginSettingsTab extends PluginSettingTab {
+ plugin: MakeMDPlugin;
+
+ constructor(app: App, plugin: MakeMDPlugin) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
+
+ refreshView() {
+ let evt = new CustomEvent(eventTypes.refreshView, {});
+ window.dispatchEvent(evt);
+ }
+
+ display(): void {
+ let { containerEl } = this;
+ containerEl.empty();
+
+ containerEl.createEl('h2', { text: t.settings.sectionSidebar });
+ new Setting(containerEl)
+ .setName(t.settings.spaces.name)
+ .setDesc(t.settings.spaces.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.spacesEnabled).onChange((value) => {
+ this.plugin.settings.spacesEnabled = value;
+ this.plugin.saveSettings();
+ if (value) {
+ this.plugin.openFileTreeLeaf(true);
+ } else {
+ this.plugin.detachFileTreeLeafs();
+ }
+ this.refreshView();
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.spacesStickers.name)
+ .setDesc(t.settings.spacesStickers.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.spacesStickers).onChange((value) => {
+ this.plugin.settings.spacesStickers = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.sidebarRibbon.name)
+ .setDesc(t.settings.sidebarRibbon.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.sidebarRibbon).onChange((value) => {
+ this.plugin.settings.sidebarRibbon = value;
+ this.plugin.saveSettings();
+ document.body.classList.toggle('mk-hide-ribbon', !value);
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.sidebarTabs.name)
+ .setDesc(t.settings.sidebarTabs.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.sidebarTabs).onChange((value) => {
+ this.plugin.settings.sidebarTabs = value;
+ this.plugin.saveSettings();
+ document.body.classList.toggle('mk-hide-tabs', !value);
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.spacesPerformance.name)
+ .setDesc(t.settings.spacesPerformance.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.spacesPerformance).onChange((value) => {
+ this.plugin.settings.spacesPerformance = value;
+ this.plugin.saveSettings();
+ })
+ );
+
+ new Setting(containerEl)
+ .setName(t.settings.spacesDeleteOption.name)
+ .setDesc(t.settings.spacesDeleteOption.desc)
+ .addDropdown((dropdown) => {
+ dropdown.addOption('permanent', t.settings.spacesDeleteOptions.permanant);
+ dropdown.addOption('trash', t.settings.spacesDeleteOptions.trash);
+ dropdown.addOption('system-trash', t.settings.spacesDeleteOptions['system-trash']);
+ dropdown.setValue(this.plugin.settings.deleteFileOption);
+ dropdown.onChange((option: DeleteFileOption) => {
+ this.plugin.settings.deleteFileOption = option;
+ this.plugin.saveSettings();
+ });
+ });
+
+ containerEl.createEl('h2', { text: t.settings.sectionFlow });
+
+ new Setting(containerEl)
+ .setName(t.settings.editorFlowReplace.name)
+ .setDesc(t.settings.editorFlowReplace.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.editorFlow).onChange((value) => {
+ this.plugin.settings.editorFlow = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.editorFlowStyle.name)
+ .setDesc(t.settings.editorFlowStyle.desc)
+ .addDropdown((dropdown: DropdownComponent) => {
+ dropdown.addOption("classic", t.settings.editorFlowStyle.classic);
+ dropdown.addOption("seamless", t.settings.editorFlowStyle.seamless);
+ dropdown
+ .setValue(this.plugin.settings.editorFlowStyle)
+ .onChange(async (value) => {
+ this.plugin.settings.editorFlowStyle = value;
+ document.body.classList.toggle('mk-flow-classic', false);
+ document.body.classList.toggle('mk-flow-seamless', false);
+ if (value == 'seamless')
+ document.body.classList.toggle('mk-flow-seamless', true);
+ if (value == 'classic')
+ document.body.classList.toggle('mk-flow-classic', true);
+ });
+ });
+
+ containerEl.createEl('h2', { text: t.settings.sectionEditor });
+
+ new Setting(containerEl)
+ .setName(t.settings.makeChar.name)
+ .setDesc(t.settings.makeChar.desc)
+ .addText(text => {
+ text.setValue(this.plugin.settings.menuTriggerChar).onChange(
+ async value => {
+ if (value.trim().length < 1) {
+ text.setValue(this.plugin.settings.menuTriggerChar)
+ return
+ }
+
+ let char = value[0]
+
+ if (value.trim().length === 2) {
+ char = value.replace(
+ this.plugin.settings.menuTriggerChar,
+ ""
+ )
+ }
+
+ text.setValue(char)
+
+ this.plugin.settings.menuTriggerChar = char
+
+ await this.plugin.saveSettings()
+ }
+ )
+ })
+
+
+ new Setting(containerEl)
+ .setName(t.settings.editorMakePlacholder.name)
+ .setDesc(t.settings.editorMakePlacholder.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.makeMenuPlaceholder).onChange((value) => {
+ this.plugin.settings.makeMenuPlaceholder = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+
+ new Setting(containerEl)
+ .setName(t.settings.mobileMakeBar.name)
+ .setDesc(t.settings.mobileMakeBar.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.mobileMakeBar).onChange((value) => {
+ this.plugin.settings.mobileMakeBar = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+
+ new Setting(containerEl)
+ .setName(t.settings.inlineStyler.name)
+ .setDesc(t.settings.inlineStyler.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.inlineStyler).onChange((value) => {
+ this.plugin.settings.inlineStyler = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.inlineStylerColor.name)
+ .setDesc(t.settings.inlineStylerColor.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.inlineStylerColors).onChange((value) => {
+ this.plugin.settings.inlineStylerColors = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+ new Setting(containerEl)
+ .setName(t.settings.editorMarkSans.name)
+ .setDesc(t.settings.editorMarkSans.desc)
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.markSans).onChange((value) => {
+ this.plugin.settings.markSans = value;
+ this.plugin.saveSettings();
+ this.refreshView();
+ })
+ );
+
+ }
+}
diff --git a/src/types/make.d.ts b/src/types/make.d.ts
new file mode 100644
index 0000000..38091f4
--- /dev/null
+++ b/src/types/make.d.ts
@@ -0,0 +1,9 @@
+import MakeMDPlugin from "main";
+
+export {};
+
+declare global {
+ interface Window {
+ make: MakeMDPlugin;
+ }
+}
\ No newline at end of file
diff --git a/src/types/obsidian.d.ts b/src/types/obsidian.d.ts
new file mode 100644
index 0000000..1d83548
--- /dev/null
+++ b/src/types/obsidian.d.ts
@@ -0,0 +1,110 @@
+import { FlowEditorParent } from "components/FlowEditor/FlowEditor";
+import { EditorView } from '@codemirror/view'
+
+declare module "obsidian" {
+ interface App {
+ commands: {
+ listCommands(): Command[],
+ findCommand(id: string): Command,
+ removeCommand(id: string): void,
+ executeCommandById(id: string): void,
+ commands: Record,
+ },
+ mobileToolbar: {
+ containerEl: HTMLElement;
+ }
+ hotkeyManager: {
+ getHotkeys(id: string): Hotkey[],
+ getDefaultHotkeys(id: string): Hotkey[],
+ },
+ internalPlugins: {
+ getPluginById(id: string): { instance: { options: { pinned: [] } } },
+ }
+ }
+
+ interface MetadataCache {
+ getCachedFiles(): string[],
+ getTags(): Record;
+ }
+
+ class FileExplorerPlugin extends Plugin_2 {
+ revealInFolder(this: any, ...args: any[]): any;
+ }
+
+ interface WorkspaceParent {
+ insertChild(index: number, child: WorkspaceItem, resize?: boolean): void;
+ replaceChild(index: number, child: WorkspaceItem, resize?: boolean): void;
+ removeChild(leaf: WorkspaceLeaf, resize?: boolean): void;
+ containerEl: HTMLElement;
+ }
+
+interface EmptyView extends View {
+ actionListEl: HTMLElement;
+ emptyTitleEl: HTMLElement;
+ }
+
+ interface MousePos {
+ x: number;
+ y: number;
+ }
+
+ interface EphemeralState {
+ focus?: boolean;
+ subpath?: string;
+ line?: number;
+ startLoc?: Loc;
+ endLoc?: Loc;
+ scroll?: number;
+ }
+interface WorkspaceMobileDrawer {
+ currentTab: number;
+ children: WorkspaceLeaf[];
+}
+interface WorkspaceRibbon {
+ orderedRibbonActions: any[];
+}
+interface HoverPopover {
+ parent: FlowEditorParent | null;
+ targetEl: HTMLElement;
+ hoverEl: HTMLElement;
+ hide(): void;
+ show(): void;
+ shouldShowSelf(): boolean;
+ timer: number;
+ waitTime: number;
+ shouldShow(): boolean;
+ transition(): void;
+ }
+ interface Workspace {
+ recordHistory(leaf: WorkspaceLeaf, pushHistory: boolean): void;
+ iterateLeaves(callback: (item: WorkspaceLeaf) => boolean | void, item: WorkspaceItem | WorkspaceItem[]): boolean;
+ iterateLeaves(item: WorkspaceItem | WorkspaceItem[], callback: (item: WorkspaceLeaf) => boolean | void): boolean;
+ getDropLocation(event: MouseEvent): {
+ target: WorkspaceItem;
+ sidedock: boolean;
+ };
+ recursiveGetTarget(event: MouseEvent, parent: WorkspaceParent): WorkspaceItem;
+ recordMostRecentOpenedFile(file: TFile): void;
+ onDragLeaf(event: MouseEvent, leaf: WorkspaceLeaf): void;
+ onLayoutChange(): void // tell Obsidian leaves have been added/removed/etc.
+ floatingSplit: WorkspaceSplit;
+ }
+interface WorkspaceSplit {
+ children: any[];
+}
+ interface WorkspaceLeaf {
+ containerEl: HTMLElement;
+ tabHeaderInnerTitleEl: HTMLElement;
+ }
+ interface Editor {
+ cm: EditorView;
+ }
+
+interface View {
+ headerEl: HTMLDivElement;
+}
+
+ interface EditorSuggest {
+ suggestEl: HTMLElement;
+ }
+}
\ No newline at end of file
diff --git a/src/types/types.ts b/src/types/types.ts
new file mode 100644
index 0000000..fae3954
--- /dev/null
+++ b/src/types/types.ts
@@ -0,0 +1,86 @@
+import { TFolder, TFile } from 'obsidian';
+import { UniqueIdentifier } from "@dnd-kit/core";
+
+export interface SectionTree {
+ section: string;
+ children: string[];
+ collapsed: boolean;
+}
+
+
+export interface StringTree {
+ node: string
+ children: StringTree[]
+ isFolder: boolean
+}
+
+export interface StringTreePath extends StringTree {
+ path: string
+}
+
+export interface FolderTree extends TFolder {
+ id: UniqueIdentifier;
+ isFolder: boolean
+}
+
+export interface FlattenedTreeNode extends FolderTree {
+ parentId: UniqueIdentifier | null;
+ depth: number;
+ index: number;
+ section: number;
+ }
+
+
+export const eventTypes = {
+ activeFileChange: 'mkmd-active-file-change',
+ refreshView: 'mkmd-refresh-view',
+ revealFile: 'mkmd-reveal-file',
+ vaultChange: 'mkmd-vault-change',
+ updateSections: 'mkmd-update-sections',
+ settingsChanged: 'mkmd-settings-changed',
+ spawnPortal: 'mkmd-portal-spawn',
+ openFilePortal: 'mkmd-portal-file',
+ focusPortal: 'mkmd-portal-focus',
+};
+
+export type VaultChange = 'create' | 'delete' | 'rename' | 'modify' | 'collapse';
+export type PortalType = 'none' | 'doc' | 'block' | 'callout' | 'flow';
+
+export class CustomVaultChangeEvent extends Event {
+ detail: {
+ file: TFile;
+ changeType: VaultChange;
+ oldPath: string;
+ };
+}
+
+export class SpawnPortalEvent extends Event {
+ detail: {
+ el: HTMLElement;
+ file: string;
+ from?: number;
+ to?: number;
+ type: PortalType;
+ id: string;
+ };
+}
+
+export class OpenFilePortalEvent extends Event {
+ detail: {
+ file: string;
+ source: string;
+ };
+}
+
+export class FocusPortalEvent extends Event {
+ detail: {
+ id: string;
+ parent: boolean;
+ top: boolean;
+ };
+}
+
+export type TransactionRange = {
+ from: number;
+ to: number;
+}
\ No newline at end of file
diff --git a/src/utils/autosizer.tsx b/src/utils/autosizer.tsx
new file mode 100644
index 0000000..d0ba016
--- /dev/null
+++ b/src/utils/autosizer.tsx
@@ -0,0 +1,195 @@
+
+import * as React from 'react';
+import createDetectElementResize from './detectElementResize';
+//fixed autosizer offscreen scroll bug
+type Size = {
+ height: number,
+ width: number,
+};
+
+type Props = {
+ /** Function responsible for rendering children.*/
+ children: (size: Size) => JSX.Element,
+
+ /** Optional custom CSS class name to attach to root AutoSizer element. */
+ className?: string,
+
+ /** Default height to use for initial render; useful for SSR */
+ defaultHeight?: number,
+
+ /** Default width to use for initial render; useful for SSR */
+ defaultWidth?: number,
+
+ /** Disable dynamic :height property */
+ disableHeight: boolean,
+
+ /** Disable dynamic :width property */
+ disableWidth: boolean,
+
+ /** Nonce of the inlined stylesheet for Content Security Policy */
+ nonce?: string,
+
+ /** Callback to be invoked on-resize */
+ onResize: (size: Size) => void,
+
+ /** Optional inline style */
+ style?: React.CSSProperties,
+};
+
+type State = {
+ height: number,
+ width: number,
+};
+
+type ResizeHandler = (element: HTMLElement, onResize: () => void) => void;
+
+type DetectElementResize = {
+ addResizeListener: ResizeHandler,
+ removeResizeListener: ResizeHandler,
+};
+
+export default class AutoSizer extends React.PureComponent {
+ static defaultProps = {
+ onResize: () => {},
+ disableHeight: false,
+ disableWidth: false,
+ style: {},
+ };
+
+ state = {
+ height: this.props.defaultHeight || 0,
+ width: this.props.defaultWidth || 0,
+ };
+
+ _parentNode?: HTMLElement;
+ _autoSizer?: HTMLElement;
+ _detectElementResize: DetectElementResize;
+
+ _onResize = () => {
+ const { disableHeight, disableWidth, onResize } = this.props;
+
+ if (this._parentNode) {
+ // Guard against AutoSizer component being removed from the DOM immediately after being added.
+ // This can result in invalid style values which can result in NaN values if we don't handle them.
+ // See issue #150 for more context.
+
+ const height = this._parentNode.offsetHeight || 0;
+ const width = this._parentNode.offsetWidth || 0;
+
+ const style = window.getComputedStyle(this._parentNode) || {} as CSSStyleDeclaration;
+ const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
+ const paddingRight = parseInt(style.paddingRight, 10) || 0;
+ const paddingTop = parseInt(style.paddingTop, 10) || 0;
+ const paddingBottom = parseInt(style.paddingBottom, 10) || 0;
+
+ const newHeight = height - paddingTop - paddingBottom;
+ const newWidth = width - paddingLeft - paddingRight;
+ if (height == 0 || width == 0) {
+ return;
+ }
+ if (
+ (!disableHeight && this.state.height !== newHeight) ||
+ (!disableWidth && this.state.width !== newWidth)
+ ) {
+ this.setState({
+ height: height - paddingTop - paddingBottom,
+ width: width - paddingLeft - paddingRight,
+ });
+
+ onResize({ height, width });
+ }
+ }
+ };
+
+ _setRef = (autoSizer?: HTMLElement) => {
+ this._autoSizer = autoSizer;
+ };
+
+ componentDidMount() {
+ const { nonce } = this.props;
+ if (
+ this._autoSizer &&
+ this._autoSizer.parentNode &&
+ this._autoSizer.parentNode.ownerDocument &&
+ this._autoSizer.parentNode.ownerDocument.defaultView &&
+ this._autoSizer.parentNode instanceof
+ this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement
+ ) {
+ // Delay access of parentNode until mount.
+ // This handles edge-cases where the component has already been unmounted before its ref has been set,
+ // As well as libraries like react-lite which have a slightly different lifecycle.
+ this._parentNode = this._autoSizer.parentNode;
+
+ // Defer requiring resize handler in order to support server-side rendering.
+ // See issue #41
+ this._detectElementResize = createDetectElementResize(nonce);
+ this._detectElementResize.addResizeListener(
+ this._parentNode,
+ this._onResize
+ );
+
+ this._onResize();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._detectElementResize && this._parentNode) {
+ this._detectElementResize.removeResizeListener(
+ this._parentNode,
+ this._onResize
+ );
+ }
+ }
+
+ render() {
+ const {
+ children,
+ className,
+ disableHeight,
+ disableWidth,
+ style,
+ } = this.props;
+ const { height, width } = this.state;
+
+ // Outer div should not force width/height since that may prevent containers from shrinking.
+ // Inner component should overflow and use calculated width/height.
+ // See issue #68 for more information.
+ const outerStyle: React.CSSProperties = { overflow: 'visible' };
+ const childParams: Size = {} as Size;
+
+ // Avoid rendering children before the initial measurements have been collected.
+ // At best this would just be wasting cycles.
+ let bailoutOnChildren = false;
+
+ if (!disableHeight) {
+ if (height === 0) {
+ bailoutOnChildren = true;
+ }
+ outerStyle.height = 0;
+ childParams.height = height;
+ }
+
+ if (!disableWidth) {
+ if (width === 0) {
+ bailoutOnChildren = true;
+ }
+ outerStyle.width = 0;
+ childParams.width = width;
+ }
+
+ return (
+
+ {!bailoutOnChildren && children(childParams)}
+
+ );
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/utils/codemirror.ts b/src/utils/codemirror.ts
new file mode 100644
index 0000000..ea8a06b
--- /dev/null
+++ b/src/utils/codemirror.ts
@@ -0,0 +1,83 @@
+import { Decoration, EditorView } from '@codemirror/view';
+import {EditorState} from '@codemirror/state'
+import { foldedRanges, syntaxTree } from '@codemirror/language';
+import { SyntaxNodeRef } from '@lezer/common';
+import { TransactionRange } from 'types/types';
+import { MarkdownView, WorkspaceLeaf } from 'obsidian';
+
+export const getActiveCM = () : EditorView | undefined => {
+ let rcm : EditorView;
+ app.workspace.iterateLeaves((leaf) => {
+ const cm = (leaf.view as MarkdownView).editor?.cm
+ if (cm.hasFocus) {
+ rcm = cm;
+ return true;
+ }
+ }, app.workspace["rootSplit"]!)
+ return rcm;
+}
+
+export const getActiveMarkdownView = () : MarkdownView | undefined => {
+ let rv : MarkdownView;
+ app.workspace.iterateLeaves((leaf) => {
+ const cm = (leaf.view as MarkdownView).editor?.cm
+ if (cm.hasFocus) {
+ rv = (leaf.view as MarkdownView);
+ return true;
+ }
+ }, app.workspace["rootSplit"]!)
+ return rv;
+}
+
+
+export function iterateTreeInVisibleRanges(
+ view: EditorView,
+ iterateFns: {
+ enter(node: SyntaxNodeRef): boolean | void;
+ leave?(node: SyntaxNodeRef): void;
+ }
+) {
+ for (const { from, to } of view.visibleRanges) {
+ syntaxTree(view.state).iterate({ ...iterateFns, from, to });
+ }
+}
+
+//optimize with resolve later...
+export function iterateTreeAtPos(
+ pos: number,
+ state: EditorState,
+ iterateFns: {
+ enter(node: SyntaxNodeRef): boolean | void;
+ leave?(node: SyntaxNodeRef): void;
+ }
+) {
+ syntaxTree(state).iterate({ ...iterateFns, from: pos, to: pos });
+}
+
+export function iterateTreeInSelection(
+ selection: TransactionRange,
+ state: EditorState,
+ iterateFns: {
+ enter(node: SyntaxNodeRef): boolean | void;
+ leave?(node: SyntaxNodeRef): void;
+ }
+) {
+ syntaxTree(state).iterate({ ...iterateFns, from: selection.from, to: selection.to });
+}
+
+export function iterateTreeInDocument(
+ state: EditorState,
+ iterateFns: {
+ enter(node: SyntaxNodeRef): boolean | void;
+ leave?(node: SyntaxNodeRef): void;
+ }
+) {
+ syntaxTree(state).iterate({ ...iterateFns });
+}
+
+export function checkRangeOverlap(
+ range1: [number, number],
+ range2: [number, number]
+) {
+ return range1[0] <= range2[1] && range2[0] <= range1[1];
+}
\ No newline at end of file
diff --git a/src/utils/detectElementResize.js b/src/utils/detectElementResize.js
new file mode 100644
index 0000000..2810fc6
--- /dev/null
+++ b/src/utils/detectElementResize.js
@@ -0,0 +1,282 @@
+/**
+ * Detect Element Resize.
+ * https://github.com/sdecima/javascript-detect-element-resize
+ * Sebastian Decima
+ *
+ * Forked from version 0.5.3; includes the following modifications:
+ * 1) Guard against unsafe 'window' and 'document' references (to support SSR).
+ * 2) Defer initialization code via a top-level function wrapper (to support SSR).
+ * 3) Avoid unnecessary reflows by not measuring size for scroll events bubbling from children.
+ * 4) Add nonce for style element.
+ **/
+
+// Check `document` and `window` in case of server-side rendering
+let windowObject;
+if (typeof window !== 'undefined') {
+ windowObject = window;
+
+ // eslint-disable-next-line no-restricted-globals
+} else if (typeof self !== 'undefined') {
+ // eslint-disable-next-line no-restricted-globals
+ windowObject = self;
+} else {
+ windowObject = global;
+}
+
+let cancelFrame = null;
+let requestFrame = null;
+
+const TIMEOUT_DURATION = 20;
+
+const clearTimeoutFn = windowObject.clearTimeout;
+const setTimeoutFn = windowObject.setTimeout;
+
+const cancelAnimationFrameFn =
+ windowObject.cancelAnimationFrame ||
+ windowObject.mozCancelAnimationFrame ||
+ windowObject.webkitCancelAnimationFrame;
+
+const requestAnimationFrameFn =
+ windowObject.requestAnimationFrame ||
+ windowObject.mozRequestAnimationFrame ||
+ windowObject.webkitRequestAnimationFrame;
+
+if (cancelAnimationFrameFn == null || requestAnimationFrameFn == null) {
+ // For environments that don't support animation frame,
+ // fallback to a setTimeout based approach.
+ cancelFrame = clearTimeoutFn;
+ requestFrame = function requestAnimationFrameViaSetTimeout(callback) {
+ return setTimeoutFn(callback, TIMEOUT_DURATION);
+ };
+} else {
+ // Counter intuitively, environments that support animation frames can be trickier.
+ // Chrome's "Throttle non-visible cross-origin iframes" flag can prevent rAFs from being called.
+ // In this case, we should fallback to a setTimeout() implementation.
+ cancelFrame = function cancelFrame([animationFrameID, timeoutID]) {
+ cancelAnimationFrameFn(animationFrameID);
+ clearTimeoutFn(timeoutID);
+ };
+ requestFrame = function requestAnimationFrameWithSetTimeoutFallback(
+ callback
+ ) {
+ const animationFrameID = requestAnimationFrameFn(
+ function animationFrameCallback() {
+ clearTimeoutFn(timeoutID);
+ callback();
+ }
+ );
+
+ const timeoutID = setTimeoutFn(function timeoutCallback() {
+ cancelAnimationFrameFn(animationFrameID);
+ callback();
+ }, TIMEOUT_DURATION);
+
+ return [animationFrameID, timeoutID];
+ };
+}
+
+export default function createDetectElementResize(nonce) {
+ let animationKeyframes;
+ let animationName;
+ let animationStartEvent;
+ let animationStyle;
+ let checkTriggers;
+ let resetTriggers;
+ let scrollListener;
+
+ const attachEvent = typeof document !== 'undefined' && document.attachEvent;
+ if (!attachEvent) {
+ resetTriggers = function(element) {
+ const triggers = element.__resizeTriggers__,
+ expand = triggers.firstElementChild,
+ contract = triggers.lastElementChild,
+ expandChild = expand.firstElementChild;
+ contract.scrollLeft = contract.scrollWidth;
+ contract.scrollTop = contract.scrollHeight;
+ expandChild.style.width = expand.offsetWidth + 1 + 'px';
+ expandChild.style.height = expand.offsetHeight + 1 + 'px';
+ expand.scrollLeft = expand.scrollWidth;
+ expand.scrollTop = expand.scrollHeight;
+ };
+
+ checkTriggers = function(element) {
+ return (
+ element.offsetWidth !== element.__resizeLast__.width ||
+ element.offsetHeight !== element.__resizeLast__.height
+ );
+ };
+
+ scrollListener = function(e) {
+ // Don't measure (which forces) reflow for scrolls that happen inside of children!
+ if (
+ e.target.className &&
+ typeof e.target.className.indexOf === 'function' &&
+ e.target.className.indexOf('contract-trigger') < 0 &&
+ e.target.className.indexOf('expand-trigger') < 0
+ ) {
+ return;
+ }
+
+ const element = this;
+ resetTriggers(this);
+ if (this.__resizeRAF__) {
+ cancelFrame(this.__resizeRAF__);
+ }
+ this.__resizeRAF__ = requestFrame(function animationFrame() {
+ if (checkTriggers(element)) {
+ element.__resizeLast__.width = element.offsetWidth;
+ element.__resizeLast__.height = element.offsetHeight;
+ element.__resizeListeners__.forEach(function forEachResizeListener(
+ fn
+ ) {
+ fn.call(element, e);
+ });
+ }
+ });
+ };
+
+ /* Detect CSS Animations support to detect element display/re-attach */
+ let animation = false;
+ let keyframeprefix = '';
+ animationStartEvent = 'animationstart';
+ const domPrefixes = 'Webkit Moz O ms'.split(' ');
+ let startEvents = 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split(
+ ' '
+ );
+ let pfx = '';
+ {
+ const elm = document.createElement('fakeelement');
+ if (elm.style.animationName !== undefined) {
+ animation = true;
+ }
+
+ if (animation === false) {
+ for (let i = 0; i < domPrefixes.length; i++) {
+ if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
+ pfx = domPrefixes[i];
+ keyframeprefix = '-' + pfx.toLowerCase() + '-';
+ animationStartEvent = startEvents[i];
+ animation = true;
+ break;
+ }
+ }
+ }
+ }
+
+ animationName = 'resizeanim';
+ animationKeyframes =
+ '@' +
+ keyframeprefix +
+ 'keyframes ' +
+ animationName +
+ ' { from { opacity: 0; } to { opacity: 0; } } ';
+ animationStyle = keyframeprefix + 'animation: 1ms ' + animationName + '; ';
+ }
+
+ const createStyles = function(doc) {
+ if (!doc.getElementById('detectElementResize')) {
+ //opacity:0 works around a chrome bug https://code.google.com/p/chromium/issues/detail?id=286360
+ const css =
+ (animationKeyframes ? animationKeyframes : '') +
+ '.resize-triggers { ' +
+ (animationStyle ? animationStyle : '') +
+ 'visibility: hidden; opacity: 0; } ' +
+ '.resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }',
+ head = doc.head || doc.getElementsByTagName('head')[0],
+ style = doc.createElement('style');
+
+ style.id = 'detectElementResize';
+ style.type = 'text/css';
+
+ if (nonce != null) {
+ style.setAttribute('nonce', nonce);
+ }
+
+ if (style.styleSheet) {
+ style.styleSheet.cssText = css;
+ } else {
+ style.appendChild(doc.createTextNode(css));
+ }
+
+ head.appendChild(style);
+ }
+ };
+
+ const addResizeListener = function(element, fn) {
+ if (attachEvent) {
+ element.attachEvent('onresize', fn);
+ } else {
+ if (!element.__resizeTriggers__) {
+ const doc = element.ownerDocument;
+ const elementStyle = windowObject.getComputedStyle(element);
+ if (elementStyle && elementStyle.position === 'static') {
+ element.style.position = 'relative';
+ }
+ createStyles(doc);
+ element.__resizeLast__ = {};
+ element.__resizeListeners__ = [];
+ (element.__resizeTriggers__ = doc.createElement('div')).className =
+ 'resize-triggers';
+ const expandTrigger = doc.createElement('div');
+ expandTrigger.className = 'expand-trigger';
+ expandTrigger.appendChild(doc.createElement('div'));
+ const contractTrigger = doc.createElement('div');
+ contractTrigger.className = 'contract-trigger';
+ element.__resizeTriggers__.appendChild(expandTrigger);
+ element.__resizeTriggers__.appendChild(contractTrigger);
+ element.appendChild(element.__resizeTriggers__);
+ resetTriggers(element);
+ element.addEventListener('scroll', scrollListener, true);
+
+ /* Listen for a css animation to detect element display/re-attach */
+ if (animationStartEvent) {
+ element.__resizeTriggers__.__animationListener__ = function animationListener(
+ e
+ ) {
+ if (e.animationName === animationName) {
+ resetTriggers(element);
+ }
+ };
+ element.__resizeTriggers__.addEventListener(
+ animationStartEvent,
+ element.__resizeTriggers__.__animationListener__
+ );
+ }
+ }
+ element.__resizeListeners__.push(fn);
+ }
+ };
+
+ const removeResizeListener = function(element, fn) {
+ if (attachEvent) {
+ element.detachEvent('onresize', fn);
+ } else {
+ element.__resizeListeners__.splice(
+ element.__resizeListeners__.indexOf(fn),
+ 1
+ );
+ if (!element.__resizeListeners__.length) {
+ element.removeEventListener('scroll', scrollListener, true);
+ if (element.__resizeTriggers__.__animationListener__) {
+ element.__resizeTriggers__.removeEventListener(
+ animationStartEvent,
+ element.__resizeTriggers__.__animationListener__
+ );
+ element.__resizeTriggers__.__animationListener__ = null;
+ }
+ try {
+ element.__resizeTriggers__ = !element.removeChild(
+ element.__resizeTriggers__
+ );
+ } catch (e) {
+ // Preact compat; see developit/preact-compat/issues/228
+ }
+ }
+ }
+ };
+
+ return {
+ addResizeListener,
+ removeResizeListener,
+ };
+}
\ No newline at end of file
diff --git a/src/utils/flowEditor.ts b/src/utils/flowEditor.ts
new file mode 100644
index 0000000..7aec815
--- /dev/null
+++ b/src/utils/flowEditor.ts
@@ -0,0 +1,273 @@
+import { calloutField, CalloutInfo, flowIDAnnotation, flowIDStateField, flowTypeStateField, portalTypeAnnotation } from "cm-extensions/markSans/callout";
+import { cacheFlowEditorHeight, flowEditorInfo, FlowEditorInfo } from "cm-extensions/flowEditor/flowEditor";
+import { createFlowEditorInElement } from "dispatch/flowDispatch";
+import { App, Editor, TFile, WorkspaceLeaf } from "obsidian";
+import { EditorView } from '@codemirror/view'
+import { EditorSelection } from '@codemirror/state'
+import { editableRange, lineRangeToPosRange, selectiveLinesFacet } from "cm-extensions/flowEditor/selectiveEditor";
+import MakeMDPlugin from "main";
+import { FlowEditor, FlowEditorParent } from "components/FlowEditor/FlowEditor";
+import { FocusPortalEvent, OpenFilePortalEvent, SpawnPortalEvent } from "types/types";
+import { createNewMarkdownFile, openFile } from "./utils";
+import { arrowKeyAnnotation } from "cm-extensions/flowEditor/atomic";
+import t from 'i18n'
+
+const parseOutReferences = (ostr: string) : [string, string | undefined] => {
+ const str = ostr.split('|')[0]
+ const refIndex = str.lastIndexOf('#');
+ return refIndex != -1 ? [str.substring(0, refIndex), str.substring(refIndex+1)] : [str, undefined]
+ }
+
+ export const getFileFromString = (url: string, source: string) => {
+ return app.metadataCache.getFirstLinkpathDest(url, source);
+ }
+
+ const getLineRangeFromRef = (file: TFile, ref: string | undefined, app: App) : [number | undefined, number | undefined] => {
+
+ if (!ref) {
+ return [undefined, undefined];
+ }
+ const cache = app.metadataCache.getFileCache(file);
+ const headings = cache.headings
+ const blocks = cache.blocks;
+ const sections = cache.sections;
+ if (blocks && ref.charAt(0) == '^' && blocks[ref.substring(1)]) {
+ return [blocks[ref.substring(1)].position.start.line+1, blocks[ref.substring(1)].position.end.line+1]
+ }
+ const heading = headings?.find(f => f.heading.replace('#', ' ') == ref)
+ if (heading)
+ {
+ const index = headings.findIndex(f => f.heading == heading.heading);
+ const level = headings[index]?.level
+ const nextIndex = headings.findIndex((f, i) => i > index && f.level <= level)
+
+ if (index < headings.length-1 && nextIndex != -1) {
+ return [heading.position.start.line+2, headings[nextIndex].position.end.line]
+ }
+ return [heading.position.start.line+2, sections[sections.length-1].position.end.line+1]
+ }
+ return [undefined, undefined];
+ }
+
+
+
+ export const loadFlowEditorByDOM = (el: HTMLElement, view: EditorView, id: string) => {
+ setTimeout(async () => {
+ //wait for el to be attached to the displayed document
+ let counter = 0;
+ while(!el.parentElement && counter++<=50) await sleep(50);
+ if(!el.parentElement) return;
+
+
+ let dom: HTMLElement = el;
+ while (
+ (!dom.hasClass("mk-floweditor") && !dom.hasClass("workspace")) &&
+ dom.parentElement
+ ) {
+
+ dom = dom.parentElement;
+ }
+
+ if (!dom.hasClass("mk-floweditor") && !dom.hasClass("workspace")) {
+ return;
+ }
+ setTimeout(async () => {
+ //wait for el to be attached to the displayed document
+ let counter = 0;
+ while(!dom.parentElement && counter++<=50) await sleep(50);
+ if(!dom.parentElement) return;
+
+
+ app.workspace.iterateLeaves((leaf) => {
+ //@ts-ignore
+ const cm = leaf.view.editor?.cm as EditorView
+ if (cm && view.dom == cm.dom) {
+ loadFlowEditorsForLeafForID(cm, leaf, app, id)
+ }
+ }, app.workspace["rootSplit"]!)
+
+ });
+ });
+
+ }
+ export const loadFlowEditorsForLeafForID = (cm: EditorView, leaf: WorkspaceLeaf, app: App, id: string) => {
+ const stateField = cm.state.field(flowEditorInfo, false);
+ if (!stateField)
+ return;
+ const flowInfo = stateField.find(f => f.id == id)
+ if (flowInfo && flowInfo.expandedState == 2) {
+ loadFlowEditor(cm, flowInfo, leaf, app)
+ }
+ }
+
+ export const loadFlowEditorsForLeaf = (cm: EditorView, leaf: WorkspaceLeaf, app: App) => {
+ const stateField = cm.state.field(flowEditorInfo, false);
+ if (!stateField)
+ return;
+ for (let flowInfo of stateField) {
+ if (flowInfo.expandedState == 2 && flowInfo.embed <= 1) {
+ loadFlowEditor(cm, flowInfo, leaf, app)
+ }
+ }
+ }
+
+export const loadFlowEditor = (cm: EditorView, flowEditorInfo: FlowEditorInfo, leaf: WorkspaceLeaf, app: App) => {
+ const dom = cm.dom.querySelector('#mk-flow-'+flowEditorInfo.id) as HTMLElement;
+ const [link, ref] = parseOutReferences(flowEditorInfo.link);
+ //@ts-ignore
+ const source = leaf.view.file?.path;
+ const file = getFileFromString(link, source)
+ if (dom) {
+ if (file) {
+ const selectiveRange = getLineRangeFromRef(file, ref, app);
+ if (!dom.hasAttribute("ready")) {
+ // dom.empty();
+ dom.setAttribute("ready","");
+ createFlowEditorInElement(flowEditorInfo.id, dom, ref ? 'block' : 'flow', file.path, selectiveRange[0], selectiveRange[1])
+ }
+ } else {
+ dom.empty();
+ const createDiv = dom.createDiv('file-embed');
+ createDiv.toggleClass('mod-empty', true);
+ const createFile = async (e: MouseEvent) => {
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ //@ts-ignore
+ await app.fileManager.createNewMarkdownFile(app.vault.getRoot(), link);
+ loadFlowEditor(cm, flowEditorInfo, leaf, app);
+ }
+ createDiv.setText(`"${link}" `+t.labels.noFile);
+ createDiv.addEventListener('click', createFile);
+
+ }
+ }
+}
+
+export const focusPortal = async(plugin: MakeMDPlugin, evt: FocusPortalEvent) =>{
+ const {id, parent, top} = evt.detail;
+ if (parent) {
+ app.workspace.iterateLeaves((leaf) => {
+ //@ts-ignore
+ const cm = leaf.view.editor?.cm as EditorView
+ if (cm) {
+ const stateField = cm.state.field(flowEditorInfo, false);
+ if (stateField) {
+ const foundInfo = stateField.find(f => f.id == id);
+ if(foundInfo){
+ cm.focus();
+ if (top) {
+ cm.dispatch({
+ selection: EditorSelection.single(foundInfo.from-4),
+ annotations: arrowKeyAnnotation.of(1)
+ })
+ } else {
+ if (foundInfo.to+2 == cm.state.doc.length) {
+
+ cm.dispatch({
+ changes: [{from: foundInfo.to+2, to: foundInfo.to+2, insert: cm.state.lineBreak}],
+ selection: EditorSelection.single(foundInfo.to+3),
+ annotations: arrowKeyAnnotation.of(2)
+ })
+ } else {
+ cm.dispatch({
+ selection: EditorSelection.single(foundInfo.to+3),
+ annotations: arrowKeyAnnotation.of(2)
+ })
+ }
+ }
+
+ }
+ }
+ }
+ }, app.workspace["rootSplit"]!)
+ } else {
+ app.workspace.iterateLeaves((leaf) => {
+ //@ts-ignore
+ const cm = leaf.view.editor?.cm as EditorView
+ if (cm) {
+ const stateField = cm.state.field(flowIDStateField, false);
+ if (stateField && stateField == id) {
+ cm.focus();
+ const lineRange = cm.state.field(selectiveLinesFacet, false)
+ const posRange = lineRange && lineRange[0] != undefined ? lineRangeToPosRange(cm.state, lineRange) : { from: 0, to: cm.state.doc.length};
+ if (top) {
+ cm.dispatch({
+ selection: EditorSelection.single(posRange.from),
+ })
+ } else {
+
+ cm.dispatch({
+ selection: EditorSelection.single(posRange.to),
+ })
+ }
+
+ }
+ }
+ }, app.workspace["rootSplit"]!)
+
+
+ }
+
+ }
+
+ export const openFileFromPortal = (plugin: MakeMDPlugin, evt: OpenFilePortalEvent) => {
+ const {file: fullLink, source} = evt.detail;
+ const [link, ref] = parseOutReferences(fullLink);
+ const file = getFileFromString(link, source)
+ //@ts-ignore
+ openFile({...file, isFolder: false}, plugin.app, false);
+ }
+
+export const spawnNewPortal = async(plugin: MakeMDPlugin, evt: SpawnPortalEvent) =>{
+ const {file, el, from, to} = evt.detail;
+ let portalFile = plugin.app.vault.getAbstractFileByPath(file);
+ const newLeaf = spawnPortal(plugin, el, !from && portalFile.name);
+ await newLeaf.openFile(portalFile as TFile);
+ //@ts-ignore
+ const view = newLeaf.view.editor?.cm as EditorView;
+ view.dispatch({
+ annotations: [portalTypeAnnotation.of(evt.detail.type), flowIDAnnotation.of(evt.detail.id)]
+ })
+ view.dom.addEventListener('keydown', (e) => {
+ if (e.key == 'ArrowUp') {
+ if (e.metaKey == true) {
+ view.dispatch({
+ annotations: arrowKeyAnnotation.of(3)
+ })
+ } else {
+ view.dispatch({
+ annotations: arrowKeyAnnotation.of(1)
+ })
+ }
+
+ }
+ if (e.key == 'ArrowDown') {
+ if (e.metaKey == true) {
+ view.dispatch({
+ annotations: arrowKeyAnnotation.of(4)
+ })
+ } else {
+ view.dispatch({
+ annotations: arrowKeyAnnotation.of(2)
+ })
+ }
+ }
+ })
+ if (from && to) {
+ //@ts-ignore
+ newLeaf.view.editor?.cm.dispatch({
+ annotations: [editableRange.of([from, to])]
+ })
+ }
+ }
+
+ export const spawnPortal = (plugin: MakeMDPlugin, initiatingEl?: HTMLElement, fileName?: string, onShowCallback?: () => unknown): WorkspaceLeaf => {
+ const parent = plugin.app.workspace.activeLeaf as unknown as FlowEditorParent;
+ if (!initiatingEl) initiatingEl = parent.containerEl;
+ const hoverPopover = new FlowEditor(parent, initiatingEl!, plugin, undefined, onShowCallback);
+ // plugin.attachPortal(hoverPopover);
+ if (fileName)
+ hoverPopover.titleEl.textContent = fileName.substring(0, fileName.lastIndexOf('.'));;
+ return hoverPopover.attachLeaf();
+
+ }
\ No newline at end of file
diff --git a/src/utils/icons.ts b/src/utils/icons.ts
new file mode 100644
index 0000000..7215a40
--- /dev/null
+++ b/src/utils/icons.ts
@@ -0,0 +1,191 @@
+
+//DONT USE ADDICON FROM OBSIDIAN, NO SVG ATTRS
+export const uiIconSet: Record = {
+ 'mk-ui-close': ``,
+ 'mk-ui-flow-hover': ``,
+ 'mk-ui-folder': ``,
+ 'mk-ui-open-link': ``,
+ 'mk-ui-file': ``,
+'mk-ui-expand': ``,
+'mk-ui-new-folder': ``,
+'mk-ui-new-note': ``,
+'mk-ui-collapse': ``,
+'mk-ui-options': ``,
+'mk-ui-plus': ``,
+'mk-ui-collapse-sm': ``
+}
+
+export const makeIconSet: Record = {
+ 'mk-make-todo': `
+ `,
+ 'mk-make-list': `
+ `,
+ 'mk-make-ordered': `
+ `,
+ 'mk-make-h1': `
+ `,
+ 'mk-make-h2': `
+ `,
+ 'mk-make-h3': `
+ `,
+ 'mk-make-quote': `
+ `,
+ 'mk-make-hr': `
+ `,
+ 'mk-make-link': `
+ `,
+ 'mk-make-image': `
+ `,
+ 'mk-make-codeblock': `
+ `,
+ 'mk-make-callout': ``,
+ 'mk-make-note': `
+ `,
+ 'mk-make-flow': `
+ `,
+ 'mk-make-tag': `
+ `
+
+}
+
+export const mkLogo = `
+`
+
+export const markIconSet : Record = {
+ 'mk-mark-strong': `
+ `,
+ 'mk-mark-em': ``,
+ 'mk-mark-strikethrough': `
+ `,
+ 'mk-mark-code': `
+ `,
+ 'mk-mark-link': ``,
+ 'mk-mark-blocklink': ``,
+ 'mk-mark-highlight': ``,
+ 'mk-make-attach': ``,
+ 'mk-make-keyboard': ``,
+'mk-make-slash': ``,
+'mk-make-style': ``,
+'mk-mark-color': ``
+ }
diff --git a/src/utils/markdownPost.tsx b/src/utils/markdownPost.tsx
new file mode 100644
index 0000000..172027e
--- /dev/null
+++ b/src/utils/markdownPost.tsx
@@ -0,0 +1,96 @@
+import { openFileFlowEditor } from 'dispatch/flowDispatch';
+import { FlowEditorHover } from 'components/FlowEditor/FlowEditorHover';
+import { MarkdownPostProcessorContext } from 'obsidian';
+import { EditorView } from '@codemirror/view'
+import React from 'react'
+import { createRoot } from 'react-dom/client';
+import { iterateTreeInSelection } from './codemirror';
+import { flowTypeStateField } from 'cm-extensions/markSans/callout';
+
+const getCMFromElement = (el: HTMLElement) : EditorView | undefined => {
+ let dom: HTMLElement = el;
+ while (
+ !dom.hasClass("cm-editor") &&
+ dom.parentElement
+ ) {
+ dom = dom.parentElement;
+ }
+
+ if (!dom.hasClass("cm-editor")) {
+ return;
+ }
+ let rcm : EditorView;
+ app.workspace.iterateLeaves((leaf) => {
+ //@ts-ignore
+ const cm = leaf.view.editor?.cm as EditorView
+ if (cm && dom == cm.dom) {
+ rcm = cm;
+ return true;
+ }
+ }, app.workspace["rootSplit"]!)
+ return rcm;
+}
+export const replaceAllEmbed = (el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
+ let dom: HTMLElement = el;
+ setTimeout(async () => {
+ //wait for el to be attached to the displayed document
+ let counter = 0;
+ while(!el.parentElement && counter++<=50) await sleep(50);
+ if(!el.parentElement) return;
+
+ while (
+ !dom.hasClass("markdown-embed") &&
+ dom.parentElement
+ ) {
+
+ dom = dom.parentElement;
+ }
+ if (dom) {
+ var nodes = dom.querySelectorAll('.markdown-embed-link');
+ for(var i = 0; i < nodes.length; i++){
+ if(nodes[i].parentNode === dom){
+ dom.removeChild(nodes[i]);
+ const div = dom.createDiv('mk-floweditor-selector')
+ const reactEl = createRoot(div);
+
+ // const flowType = cm.state.field(flowTypeStateField, false);
+ //@ts-ignore
+ reactEl.render( {
+ const cm : EditorView = getCMFromElement(dom);
+ const pos = cm.posAtDOM(dom)
+ iterateTreeInSelection({from: pos-3, to:pos+4}, cm.state, {
+ enter: (node) => {
+ if (node.name.contains('hmd-internal-link')) {
+ if (cm.state.sliceDoc(node.from-4, node.from-3) != '!') {
+ if (cm.state.sliceDoc(node.to+2, node.to+3) != cm.state.lineBreak) {
+ cm.dispatch({
+ changes: [{
+ from: node.from-3, to: node.from-3, insert: '!'
+ }, {
+ from: node.to+2, to: node.to+2, insert: cm.state.lineBreak
+ }
+ ]
+ })
+ } else {
+ cm.dispatch({
+ changes: {
+ from: node.from-3, to: node.from-3, insert: '!'
+ }
+ });
+ }
+ }
+ }
+ }
+ })
+
+ e.stopPropagation();
+ }} openLink={(e) => {
+ e.stopPropagation();
+ openFileFlowEditor(ctx.sourcePath, '/');
+ }}
+ >)
+ };
+ }
+ }
+});
+}
\ No newline at end of file
diff --git a/src/utils/patches.ts b/src/utils/patches.ts
new file mode 100644
index 0000000..f52eb4c
--- /dev/null
+++ b/src/utils/patches.ts
@@ -0,0 +1,139 @@
+import { FILE_TREE_VIEW_TYPE } from "components/Spaces/FileTreeView";
+import { FlowEditor } from "components/FlowEditor/FlowEditor";
+import MakeMDPlugin from "main";
+import { around } from "monkey-around";
+import { EphemeralState, ViewState, Workspace, WorkspaceContainer, WorkspaceItem, WorkspaceLeaf } from "obsidian";
+
+export const patchFileExplorer = (plugin: MakeMDPlugin) => {
+
+ plugin.register(around(Workspace.prototype, {
+ getLeavesOfType(old) {
+ return function(type: unknown) {
+ if (type == 'file-explorer') {
+
+ return old.call(this, FILE_TREE_VIEW_TYPE);
+ }
+ return old.call(this, type)
+ }
+ }
+ }));
+}
+
+export const patchWorkspace = (plugin: MakeMDPlugin) => {
+ let layoutChanging = false;
+ const uninstaller = around(Workspace.prototype, {
+
+ changeLayout(old) {
+ return async function (workspace: unknown) {
+ layoutChanging = true;
+ try {
+ // Don't consider hover popovers part of the workspace while it's changing
+ await old.call(this, workspace);
+ } finally {
+ layoutChanging = false;
+ }
+ };
+ },
+
+ iterateLeaves(old) {
+ type leafIterator = (item: WorkspaceLeaf) => boolean | void;
+ return function (arg1, arg2) {
+ // Fast exit if desired leaf found
+ if (old.call(this, arg1, arg2)) return true;
+
+ // Handle old/new API parameter swap
+ let cb: leafIterator = (typeof arg1 === "function" ? arg1 : arg2) as leafIterator;
+ let parent: WorkspaceItem = (typeof arg1 === "function" ? arg2 : arg1) as WorkspaceItem;
+
+ if (!parent) return false; // <- during app startup, rootSplit can be null
+ if (layoutChanging) return false; // Don't let HEs close during workspace change
+
+ // 0.14.x doesn't have WorkspaceContainer; this can just be an instanceof check once 15.x is mandatory:
+ if (parent === app.workspace.rootSplit || (WorkspaceContainer && parent instanceof WorkspaceContainer)) {
+ for(const popover of FlowEditor.popoversForWindow((parent as WorkspaceContainer).win)) {
+ // Use old API here for compat w/0.14.x
+ if (old.call(this, cb, popover.rootSplit)) return true;
+ }
+ }
+ return false;
+ };
+ },
+ getDropLocation(old) {
+ return function getDropLocation(event: MouseEvent) {
+ for (const popover of FlowEditor.activePopovers()) {
+ const dropLoc = this.recursiveGetTarget(event, popover.rootSplit);
+ if (dropLoc) {
+ return dropLoc;
+ }
+ }
+ return old.call(this, event);
+ };
+ },
+ onDragLeaf(old) {
+ return function (event: MouseEvent, leaf: WorkspaceLeaf) {
+ const hoverPopover = FlowEditor.forLeaf(leaf);
+ return old.call(this, event, leaf);
+ };
+ },
+ });
+ plugin.register(uninstaller);
+ }
+ export const patchWorkspaceLeaf = (plugin: MakeMDPlugin) => {
+ plugin.register(
+ around(WorkspaceLeaf.prototype, {
+ getRoot(old) {
+ return function () {
+ const top = old.call(this);
+ return top.getRoot === this.getRoot ? top : top.getRoot();
+ };
+ },
+ onResize(old) {
+ return function () {
+ this.view?.onResize();
+ };
+ },
+ setViewState(old) {
+ return async function (viewState: ViewState, eState?: unknown) {
+ const result = await old.call(this, viewState, eState);
+ try {
+ const he = FlowEditor.forLeaf(this);
+ if (he) {
+ if (viewState.type) he.hoverEl.setAttribute("data-active-view-type", viewState.type);
+ const titleEl = he.hoverEl.querySelector(".popover-title");
+ if (titleEl) {
+ titleEl.textContent = this.view?.getDisplayText();
+ if (this.view?.file?.path) {
+ titleEl.setAttribute("data-path", this.view.file.path);
+ } else {
+ titleEl.removeAttribute("data-path");
+ }
+ }
+ }
+ } catch {}
+ return result;
+ };
+ },
+ setEphemeralState(old) {
+ return function (state: EphemeralState) {
+ old.call(this, state);
+ if (state.focus && this.view?.getViewType() === "empty") {
+ // Force empty (no-file) view to have focus so dialogs don't reset active pane
+ this.view.contentEl.tabIndex = -1;
+ this.view.contentEl.focus();
+ }
+ };
+ },
+ }),
+ );
+ plugin.register(
+ around(WorkspaceItem.prototype, {
+ getContainer(old) {
+ return function () {
+ if (!old) return; // 0.14.x doesn't have this
+ if (!this.parentSplit || this instanceof WorkspaceContainer) return old.call(this);
+ return this.parentSplit.getContainer();
+ };
+ },
+ })
+ );
+ }
\ No newline at end of file
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
new file mode 100644
index 0000000..d0e3de9
--- /dev/null
+++ b/src/utils/utils.ts
@@ -0,0 +1,427 @@
+import { TFile, TFolder, App, Keymap, Platform, TAbstractFile } from 'obsidian';
+import MakeMDPlugin from 'main';
+import { eventTypes, FlattenedTreeNode, FolderTree, StringTree, StringTreePath } from 'types/types';
+import { VaultChangeModal } from 'components/Spaces/modals';
+import { UniqueIdentifier } from '@dnd-kit/core';
+import { FlowView, FOLDER_VIEW_TYPE } from 'components/FlowView/FlowView';
+
+export function buildTree(flattenedItems: FlattenedTreeNode[]): TFolder {
+ const root: TFolder = {...flattenedItems.find(f => f.id == '/'), children: [], isRoot: () => true} as TFolder;
+ const items = flattenedItems.map((item) => ({...item, children: []}));
+
+ for (const item of items) {
+ const {id, children, vault, path, name, parent} = item;
+ const parentId = item.parentId ?? '/';
+ const _parent = items.find(f => f.id == parentId) ?? items.find(f => f.id == '/');
+ if (_parent) {
+ if (item.isFolder) {
+ _parent.children.push({children, vault, parent, path, name} as TFolder);
+ } else {
+ _parent.children.push({vault, parent, path, name} as TAbstractFile);
+ }
+ }
+ }
+ return { ...root, children: items.filter(f => f.parentId == '/').map(item => {
+ const {children, vault, path, name, parent} = item;
+ if (item.isFolder) {
+ return {children, vault, parent, path, name} as TFolder;
+ } else {
+ return {vault, parent, path, name} as TAbstractFile;
+ }
+ })
+} as TFolder;
+ }
+
+ export const nodeIsAncestorOfTarget = (node: FlattenedTreeNode, target: FlattenedTreeNode) => {
+ const recursive = (_node: TFolder, _target: TFolder) : boolean => {
+ if (!_target.path) {
+ return false
+ }
+ if (_target.path == '/')
+ return false;
+ if (_target.parent.path == _node.path)
+ return true;
+ return recursive(_node, _target.parent);
+ }
+ return recursive(node, target)
+ }
+
+function getMaxDepth({previousItem}: {previousItem: FlattenedTreeNode}) {
+
+ if (previousItem) {
+ if (previousItem.isFolder)
+ return previousItem.depth + 1;
+ return previousItem.depth;
+ }
+
+ return 0;
+ }
+
+ function getMinDepth({nextItem}: {nextItem: FlattenedTreeNode}) {
+ if (nextItem) {
+ return nextItem.depth;
+ }
+
+ return 0;
+ }
+
+ export function getDragDepth(offset: number, indentationWidth: number) {
+ return Math.round(offset / indentationWidth) + 1;
+ }
+
+export function getProjection(
+ items: FlattenedTreeNode[],
+ activeItem: FlattenedTreeNode,
+ overItemIndex: number,
+ previousItem: FlattenedTreeNode,
+ nextItem: FlattenedTreeNode,
+ dragDepth: number,
+ ) {
+
+
+
+ const activeIsSection = activeItem.parentId == null;
+ const overIsSection = previousItem.parentId == null;
+ if (nodeIsAncestorOfTarget(activeItem, previousItem)) {
+ return null;
+ }
+ if (activeIsSection) {
+ if(overIsSection) {
+ return {depth: 0, maxDepth: 0, minDepth: 0, overId: previousItem.id, parentId: null};
+ }
+ return null;
+ }
+
+ if (activeItem.section != previousItem.section) {
+
+ if (previousItem.section == -1) {
+ return null;
+ }
+ }
+
+ const projectedDepth = dragDepth;
+ const maxDepth = getMaxDepth({
+ previousItem,
+ });
+ const minDepth = getMinDepth({nextItem});
+ let depth = projectedDepth;
+ if (projectedDepth >= maxDepth) {
+ depth = maxDepth;
+ } else if (projectedDepth < minDepth) {
+ depth = minDepth;
+ }
+ if (previousItem.section != -1 && depth > 1)
+ {
+ return null;
+ }
+ return {depth, maxDepth, minDepth, overId: previousItem.id, parentId: getParentId()};
+
+ function getParentId() {
+ if (depth === 0 || !previousItem) {
+ return '/';
+ }
+
+ if (depth === previousItem.depth || (depth > previousItem.depth && !previousItem.isFolder)) {
+ return previousItem.parentId;
+ }
+
+ if (depth > previousItem.depth) {
+ return previousItem.id;
+ }
+
+ const newParent = items
+ .slice(0, overItemIndex)
+ .reverse()
+ .find((item) => item.depth === depth)?.parentId;
+
+ return newParent ?? null;
+ }
+ }
+
+export const flattenTrees = (
+ items: (TAbstractFile | TFolder)[],
+ section: string,
+ sectionIndex: number,
+ parentId: UniqueIdentifier | null = null,
+ depth = 0
+ ): FlattenedTreeNode[] => {
+ return items.filter(f => f).reduce((acc, item, index) => {
+ const id = parentId+'/'+item.path
+ if ((item as any).children) {
+
+ return [
+ ...acc,
+ {...item, parentId, depth, section: sectionIndex, index, id, isFolder: true},
+ ...(flattenTrees((item as any).children, section, sectionIndex, id, depth + 1)),
+ ] as FlattenedTreeNode[];
+ } else {
+
+ return [
+ ...acc,
+ {...item, parentId, depth, section: sectionIndex, index, id: id, isFolder: false}
+ ] as FlattenedTreeNode[];
+ }
+ }, []) as FlattenedTreeNode[];
+ }
+
+ export const flattenTree = (folder: TFolder, path: string, sectionIndex: number, collapsed: boolean) : FlattenedTreeNode[] => {
+ return [{
+ ...folder,
+ id: folder.path,
+ parentId: null,
+ depth: 0,
+ index: 0,
+ section: -1,
+ isFolder: true,
+ } as FlattenedTreeNode, ...!collapsed ? flattenTrees(folder.children, path, sectionIndex, path, 1) : []]
+ }
+// Helper Function to Create Folder Tree
+
+export function includeChildrenOf(
+ items: FlattenedTreeNode[],
+ ids: UniqueIdentifier[]
+ ) {
+ const excludeParentIds = items.filter(f => f.children?.length > 0 && !ids.find(i => i == f.id) && f.id != '/').map(f => f.id);
+ return items.filter((item) => {
+ if (item.parentId && excludeParentIds.includes(item.parentId)) {
+ if (item.children?.length) {
+ excludeParentIds.push(item.id);
+ }
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+export const sortFolderTree = async (folderTree: TFolder, plugin: MakeMDPlugin) : Promise => {
+
+ const stringTree = plugin.settings.folderRank;
+ const rawStringTree = folderTreeToStringTree(folderTree);
+ const newStringTree = mergeStringTree(stringTree, rawStringTree);
+ plugin.settings.folderRank = newStringTree;
+
+ plugin.saveSettings(false);
+
+ const sortedFolderTree = sortFolderTreeUsingStringTree(folderTree, newStringTree);
+ return sortedFolderTree;
+}
+
+export const renamePathInStringTree = (oldPath: string, newFile: TAbstractFile, plugin: MakeMDPlugin) => {
+ const stringTree = plugin.settings.folderRank;
+ const newName = newFile.name;
+ const newPath = newFile.path;
+ const recursive = (tree: StringTree, path: string, oldS:string, newS:string) : StringTree => {
+
+ if (path == oldS) {
+ return {
+ ...tree,
+ node: newS
+ }
+ } else if (!tree.isFolder) {
+ return tree;
+ }
+ return {
+ ...tree,
+ children: tree.children.map(f => recursive(f, path+'/'+f.node, oldS, newS))
+ }
+ }
+ plugin.settings.fileIcons = plugin.settings.fileIcons.map(f => f[0] == oldPath ? [newPath, f[1]] : f);
+ plugin.settings.spaces = plugin.settings.spaces.map(f => {return {... f, children: f.children.map(
+ g => g == oldPath ? newPath : g
+ )}});
+
+ plugin.settings.folderRank = recursive(stringTree, '', '/'+oldPath, newName)
+
+ plugin.saveSettings();
+}
+
+export const folderTreeToStringTree = (tree: TFolder) : StringTree => {
+ const recursive = (subtree: TAbstractFile) : StringTree => {
+ if ((subtree as any).children) {
+ return {
+ node: subtree.name,
+ children: (subtree as any).children.map((f: TAbstractFile) => recursive(f)),
+ isFolder: true
+ }
+ } else {
+ return {
+ node: subtree.name,
+ children: [],
+ isFolder: false
+ }
+ }
+ }
+ return recursive(tree);
+}
+
+const reorderStringTree = (savedTrees: StringTree[], rawTrees: StringTree[]) : StringTree[] => {
+ //find missing trees in live not in cache and append
+ const missingTrees = rawTrees.filter((f => !savedTrees.find(g => f.node == g.node)))
+ const allTrees = [...savedTrees, ...missingTrees];
+ //remove trees that are in cache but not in live
+ const filteredTrees = allTrees.filter((f => rawTrees.find(g => f.node == g.node)))
+ return filteredTrees;
+}
+
+export const mergeStringTree = (savedTree: StringTree, rawTree: StringTree) : StringTree => {
+ const flattenSavedTree = (tree: StringTree) : StringTreePath[] => {
+ const treeReduce = (t: StringTree[], currPath: string) : StringTreePath[] => {
+ return t.reduce((p: StringTreePath[], c: StringTree) => {
+ return [...p, {
+ ...c,
+ path: currPath+'/'+c.node
+ }, ...treeReduce(c.children, currPath+'/'+c.node)];
+ }, [])
+ }
+ return [
+ {...tree, path: '/'},
+ ...treeReduce(tree.children, '/')];
+ }
+ const rankReferences = flattenSavedTree(savedTree);
+
+ const recursive = (subtree: StringTree, treePaths: StringTreePath[], currPath: string) : StringTree => {
+ const existingTree : StringTreePath | undefined = treePaths.find(f => currPath == f.path)
+
+ if (existingTree) {
+ return {
+ ...subtree,
+ children: reorderStringTree(existingTree.children, subtree.children).map(t => recursive(t, treePaths, currPath+'/'+t.node))
+ }
+ } else {
+ return {
+ ...subtree,
+ children: subtree.children.map(t => recursive(t, treePaths, currPath+'/'+t.node)),
+ }
+ }
+ }
+ return recursive(rawTree, rankReferences, '/')
+}
+
+export const sortFolderTreeUsingStringTree = (folderTree: TFolder, stringTree: StringTree) : TFolder => {
+ const recursiveSort = (file: TAbstractFile, strings: StringTree) : TAbstractFile | TFolder => {
+ if (file instanceof TFolder) {
+ return {
+ ...file,
+ children: file.children.map(f => {
+
+ const currStringTree = strings.children.find(g => g.node == f.name);
+
+ if (currStringTree)
+ return recursiveSort(f, currStringTree);
+ return f;
+ }).sort((a, b) => strings.children.findIndex(x => x.node == a.name)-strings.children.findIndex(x => x.node == b.name)),
+ }
+ } else {
+ return file
+ }
+ }
+ return recursiveSort(folderTree, stringTree) as TFolder;
+}
+
+export const hasChildFolder = (folder: TFolder): boolean => {
+ let children = folder.children;
+ for (let child of children) {
+ if (child instanceof TFolder) return true;
+ }
+ return false;
+};
+
+// Files out of Md should be listed with extension badge - Md without extension
+export const getFileNameAndExtension = (fullName: string) => {
+ var index = fullName.lastIndexOf('.');
+ return {
+ fileName: fullName.substring(0, index),
+ extension: fullName.substring(index + 1),
+ };
+};
+
+// Returns all parent folder paths
+export const getParentFolderPaths = (file: TFile): string[] => {
+ let folderPaths: string[] = ['/'];
+ let parts: string[] = file.parent.path.split('/');
+ let current: string = '';
+ for (let i = 0; i < parts.length; i++) {
+ current += `${i === 0 ? '' : '/'}` + parts[i];
+ folderPaths.push(current);
+ }
+ return folderPaths;
+};
+
+// Extracts the Folder Name from the Full Folder Path
+export const getFolderName = (folderPath: string, app: App) => {
+ if (folderPath === '/') return app.vault.getName();
+ let index = folderPath.lastIndexOf('/');
+ if (index !== -1) return folderPath.substring(index + 1);
+ return folderPath;
+};
+
+export const internalPluginLoaded = (pluginName: string, app: App) => {
+ // @ts-ignore
+ return app.internalPlugins.plugins[pluginName]?._loaded;
+};
+
+export const openFile = async (file: FolderTree, app: App, newLeaf: boolean) => {
+ if (file.isFolder) {
+ let leaf = app.workspace.getLeaf(newLeaf);
+ app.workspace.setActiveLeaf(leaf, {focus: true});
+ await leaf.setViewState({ type: FOLDER_VIEW_TYPE, state: { folder: file.path }})
+ await app.workspace.requestSaveLayout()
+ } else {
+ let leaf = app.workspace.getLeaf(newLeaf);
+ app.workspace.setActiveLeaf(leaf, {focus: true});
+ await leaf.openFile(app.vault.getAbstractFileByPath(file.path) as TFile, { eState: { focus: true } });
+ }
+};
+
+export const openInternalLink = (event: React.MouseEvent, link: string, app: App) => {
+ app.workspace.openLinkText(link, '/', Keymap.isModifier(event as unknown as MouseEvent, 'Mod') || 1 === event.button);
+};
+
+export const openFileInNewPane = (plugin: MakeMDPlugin, file: FlattenedTreeNode) => {
+ openFile(file, plugin.app, true);
+};
+
+
+function selectElementContents(el: Element) {
+ var range = document.createRange();
+ range.selectNodeContents(el);
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+}
+
+export const createNewMarkdownFile = async (app: App, folder: TFolder, newFileName: string, content?: string) : Promise => {
+ // @ts-ignore
+ const newFile = await app.fileManager.createNewMarkdownFile(folder, newFileName);
+ if (content && content !== '') await app.vault.modify(newFile, content);
+
+ await openFile(newFile, app, false);
+ const titleEl = app.workspace.activeLeaf.view.containerEl.querySelector('.inline-title') as HTMLDivElement;
+ if (titleEl) {
+ titleEl.focus();
+ selectElementContents(titleEl)
+ }
+ let evt = new CustomEvent(eventTypes.activeFileChange, { detail: { filePath: newFile.path } });
+ window.dispatchEvent(evt);
+ return newFile;
+};
+
+export const platformIsMobile = () => {
+ return Platform.isMobile;
+};
+
+export const createNewFile = async (e: React.MouseEvent, folderPath: string, plugin: MakeMDPlugin) => {
+ let targetFolder = plugin.app.vault.getAbstractFileByPath(folderPath);
+ if (!targetFolder) return;
+ let modal = new VaultChangeModal(plugin, targetFolder, 'create note');
+ modal.open();
+};
+
+
+export const unifiedToNative = (unified: string) => {
+ let unicodes = unified.split('-')
+ let codePoints = unicodes.map((u) => `0x${u}`)
+ // @ts-ignore
+ return String.fromCodePoint(...codePoints)
+}
\ No newline at end of file
diff --git a/styles.css b/styles.css
index 84ba9ea..404f5b0 100644
--- a/styles.css
+++ b/styles.css
@@ -422,8 +422,13 @@ body:not(.is-mobile) .mk-tree-wrapper:hover .mk-folder-buttons {
height: 20px;
color: var(--text-faint);
}
+.is-mobile .mk-tree-wrapper .mk-file-icon svg {
+ width: 18px;
+ height: 18px;
+ color: var(--text-faint);
+}
.mk-tree-text {
- padding: 0.15rem 0.5rem;
+ padding: 0.15rem 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -586,7 +591,7 @@ body:not(.is-mobile) .mk-section:hover .mk-collapse {
padding: unset;
}
.is-mobile .mk-sidebar .mk-file-icon button {
- font-size: 20px;
+ font-size: 16px;
}
body.is-mobile .sidebar-toggle-button {
display: flex !important;
@@ -634,6 +639,9 @@ body.is-mobile .sidebar-toggle-button {
font-size: var(--font-ui-medium);
font-weight: var(--font-medium);
}
+.mk-main-menu-button > div {
+ display: flex;
+}
.mk-main-menu-button svg {
height: 16px;
width: 16px;
@@ -697,6 +705,10 @@ body:not(.is-mobile) .mk-main-menu-button:hover {
align-content: flex-start;
flex-direction: row;
}
+.mk-sticker-modal {
+ display: flex;
+ flex-wrap: wrap;
+}
.mk-sticker-menu .suggestion-item {
width: 30px;
height: 30px;
@@ -760,6 +772,10 @@ body:not(.is-mobile) .mk-main-menu-button:hover {
background: var(--color-base-10);
border-bottom: thin solid #333;
}
+.mk-flow-replace .mk-new-file:hover {
+ background: var(--color-base-10);
+ border-bottom: thin solid #333;
+}
.mobile-toolbar-options-container {
border-top: var(--divider-width) solid var(--divider-color);
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e8a5ab8
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "jsx": "react",
+ "esModuleInterop": true,
+ "baseUrl": "src",
+ "inlineSourceMap": true,
+ "inlineSources": true,
+ "isolatedModules": true,
+ "module": "ESNext",
+ "target": "es6",
+ "allowJs": true,
+ "noImplicitAny": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "lib": ["dom", "es5", "scripthost", "es2015"]
+ },
+ "include": ["**/*.ts", "**/*.tsx", "src/components/Spaces/FileTreeView.tsx", "src/utils/autosizer.js", "src/utils/autosizer.js", "src/utils/detectElementResize.js"]
+}