aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/RichTextRules.ts
diff options
context:
space:
mode:
authorStanley Yip <stanley_yip@brown.edu>2020-02-09 14:58:57 -0500
committerStanley Yip <stanley_yip@brown.edu>2020-02-09 14:58:57 -0500
commitf6179334d6f2942631caa17b7c8ae2531d87c7c4 (patch)
tree091da0ef7bedb900c958c28cebe4058fade644cf /src/client/util/RichTextRules.ts
parent07141291bee793955d7061f4e479942d7aceda67 (diff)
parent87167fd126e161b29d8d798a5f04e3cf159aae16 (diff)
recommender system works
Diffstat (limited to 'src/client/util/RichTextRules.ts')
-rw-r--r--src/client/util/RichTextRules.ts266
1 files changed, 180 insertions, 86 deletions
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index ebb9bda8a..8411cc6ee 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -2,11 +2,16 @@ import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from
import { schema } from "./RichTextSchema";
import { wrappingInputRule } from "./prosemirrorPatches";
import { NodeSelection, TextSelection } from "prosemirror-state";
-import { NumCast, Cast } from "../../new_fields/Types";
-import { Doc } from "../../new_fields/Doc";
+import { StrCast, Cast, NumCast } from "../../new_fields/Types";
+import { Doc, DataSym } from "../../new_fields/Doc";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
import { Id } from "../../new_fields/FieldSymbols";
+import { DocServer } from "../DocServer";
+import { returnFalse, Utils } from "../../Utils";
+import RichTextMenu from "./RichTextMenu";
+import { RichTextField } from "../../new_fields/RichTextField";
+import { ComputedField } from "../../new_fields/ScriptField";
export const inpRules = {
rules: [
@@ -59,137 +64,226 @@ export const inpRules = {
}
),
+ // set the font size using #<font-size>
new InputRule(
- new RegExp(/^#([0-9]+)\s$/),
+ new RegExp(/%([0-9]+)\s$/),
(state, match, start, end) => {
- let size = Number(match[1]);
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
- if (ruleProvider && heading) {
- (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
- return state.tr.deleteRange(start, end);
+ const size = Number(match[1]);
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
+ }),
+
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+ new InputRule(
+ new RegExp(/\[\[([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\]\]$/),
+ (state, match, start, end) => {
+ if (!match[2]) {
+ const docId = match[1];
+ DocServer.GetRefField(docId).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true, }, docId);
+ DocUtils.Publish(target, docId, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
+ });
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docId), location: "onRight", title: docId, targetId: docId });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
}
- return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: match[2]?.substring(1), docid: match[1] });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+ new InputRule(
+ new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ (state, match, start, end) => {
+ const docId = match[1];
+ DocServer.GetRefField(docId).then(docx => {
+ if (!(docx instanceof Doc && docx)) {
+ const docx = Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true }, docId);
+ DocUtils.Publish(docx, docId, returnFalse, returnFalse);
+ }
+ });
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid: docId, float: "right", fieldKey: match[2]?.substring(1), alias: Utils.GenerateGuid() });
+ const sm = state.storedMarks || undefined;
+ return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}),
new InputRule(
- new RegExp(/t/),
+ new RegExp(/##$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ const schemaDoc = Doc.GetDataDoc((schema as any).Document);
+ const textDoc = Doc.GetProto(Cast(schemaDoc[DataSym], Doc, null)!);
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ textDoc.inlineTextCount = numInlines + 1;
+ const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
+ const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
+ const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, fontSize: 9, title: "inline comment" });
+ textDocInline.title = inlineFieldKey; // give the annotation its own title
+ textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
+ textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
+ textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
+ textDocInline._textContext = ComputedField.MakeFunction(`copyField(this.${inlineFieldKey})`, { this: Doc.name });
+ textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
+ textDoc[inlineFieldKey] = ""; // set a default value for the annotation
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" });
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced;
}),
+ // stop using active style
new InputRule(
- new RegExp(/i/),
+ new RegExp(/%%$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ const tr = state.tr.deleteRange(start, end);
+ const marks = state.tr.selection.$anchor.nodeBefore?.marks;
+ return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
}),
+
+ // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/\!/),
+ new RegExp(/[ti!x]$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null;
+ const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
}),
+
+ // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/\x/),
+ new RegExp(/(%d|d)$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
}),
+
+ // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/^\^\^\s$/),
+ new RegExp(/(%h|h)$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleAlign_" + heading] = "center";
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
}
- let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ return null;
}),
+ // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/^\[\[\s$/),
+ new RegExp(/(%q|q)$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleAlign_" + heading] = "left";
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
+ const node = state.selection.node;
+ return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
}
- let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+
+
+ // center justify text
+ new InputRule(
+ new RegExp(/%\^$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
+ // left justify text
new InputRule(
- new RegExp(/^\]\]\s$/),
+ new RegExp(/%\[$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleAlign_" + heading] = "right";
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }
- let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
+ // right justify text
new InputRule(
- new RegExp(/##\s$/),
+ new RegExp(/%\]$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
- let replaced = node ? state.tr.insertText("←", start).replaceRangeWith(start + 1, end + 1, schema.nodes.dashDoc.create({
- width: 75, height: 35,
- title: "dashDoc", docid: target[Id],
- float: "right"
- })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 1)));
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
new InputRule(
- new RegExp(/\(\(/),
+ new RegExp(/%\(/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let mark = state.schema.marks.highlight.create();
- let selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
- let content = selected.selection.content();
- let replaced = node ? selected.replaceRangeWith(start, start,
- schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || [];
+ const mark = state.schema.marks.summarizeInclusive.create();
+ sm.push(mark);
+ const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ const content = selected.selection.content();
+ const replaced = node ? selected.replaceRangeWith(start, end,
+ schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) :
state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1)));
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
}),
new InputRule(
- new RegExp(/\)\)/),
+ new RegExp(/%\)/),
(state, match, start, end) => {
- let mark = state.schema.marks.highlight.create();
- return state.tr.removeStoredMark(mark);
+ return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
}),
new InputRule(
- new RegExp(/\^f\s$/),
+ new RegExp(/%f$/),
(state, match, start, end) => {
- let newNode = schema.nodes.footnote.create({});
- let tr = state.tr;
+ const newNode = schema.nodes.footnote.create({});
+ const tr = state.tr;
tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
return tr.setSelection(new NodeSelection( // select the footnote node to open its display
tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
}),
- // let newNode = schema.nodes.footnote.create({});
- // if (dispatch && state.selection.from === state.selection.to) {
- // return true;
- // }
+
+ // activate a style by name using prefix '%'
+ new InputRule(
+ new RegExp(/%[a-z]+$/),
+ (state, match, start, end) => {
+ const color = match[0].substring(1, match[0].length);
+ const marks = RichTextMenu.Instance._brushMap.get(color);
+ if (marks) {
+ const tr = state.tr.deleteRange(start, end);
+ return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
+ }
+ const isValidColor = (strColor: string) => {
+ const s = new Option().style;
+ s.color = strColor;
+ return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
+ };
+ if (isValidColor(color)) {
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+ }
+ return null;
+ }),
]
};