<commit_before>/*
 * node.ts
 *
 * Copyright (C) 2020 by RStudio, PBC
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

import { Node as ProsemirrorNode, NodeSpec, NodeType, ResolvedPos } from 'prosemirror-model';
import { EditorState, Selection, NodeSelection } from 'prosemirror-state';
import {
  findParentNode,
  findSelectedNodeOfType,
  ContentNodeWithPos,
  NodeWithPos,
  findParentNodeOfType,
  findChildrenByType,
  findChildren,
  findParentNodeOfTypeClosestToPos,
} from 'prosemirror-utils';

import { EditorView } from 'prosemirror-view';

import {
  PandocTokenReader,
  PandocNodeWriterFn,
  PandocPreprocessorFn,
  PandocBlockReaderFn,
  PandocInlineHTMLReaderFn,
} from './pandoc';
import { PandocBlockCapsuleFilter } from './pandoc_capsule';

import { AttrEditOptions } from './attr_edit';
import { CommandFn } from './command';
import { ExecuteRmdChunkFn } from './rmd';

export interface PandocNode {
  readonly name: string;
  readonly spec: NodeSpec;
  readonly code_view?: CodeViewOptions;
  readonly attr_edit?: () => AttrEditOptions | null;
  readonly pandoc: {
    readonly readers?: readonly PandocTokenReader[];
    readonly writer?: PandocNodeWriterFn;
    readonly preprocessor?: PandocPreprocessorFn;
    readonly blockReader?: PandocBlockReaderFn;
    readonly inlineHTMLReader?: PandocInlineHTMLReaderFn;
    readonly blockCapsuleFilter?: PandocBlockCapsuleFilter;
  };
}

export interface CodeViewOptions {
  lang: (node: ProsemirrorNode, content: string) => string | null;
  attrEditFn?: CommandFn;
  executeRmdChunkFn?: ExecuteRmdChunkFn;
  classes?: string[];
  borderColorClass?: string;
  firstLineMeta?: boolean;
  lineNumbers?: boolean;
  bookdownTheorems?: boolean;
  lineNumberFormatter?: (lineNumber: number, lineCount?: number, line?: string) => string;
}

export type NodeTraversalFn = (
  node: Node,
  pos: number,
  parent: Node,
  index: number,
) => boolean | void | null | undefined;

export function findTopLevelBodyNodes(doc: ProsemirrorNode, predicate: (node: ProsemirrorNode) => boolean) {
  const body = findChildrenByType(doc, doc.type.schema.nodes.body, false)[0];
  const offset = body.pos + 1;
  const nodes = findChildren(body.node, predicate, false);
  return nodes.map(value => ({ ...value, pos: value.pos + offset }));
}

export function findNodeOfTypeInSelection(selection: Selection, type: NodeType): ContentNodeWithPos | undefined {
  return findSelectedNodeOfType(type)(selection) || findParentNode((n: ProsemirrorNode) => n.type === type)(selection);
}

export function firstNode(parent: NodeWithPos, predicate: (node: ProsemirrorNode) => boolean) {
  let foundNode: NodeWithPos | undefined;
  parent.node.descendants((node, pos) => {
    if (!foundNode) {
      if (predicate(node)) {
        foundNode = {
          node,
          pos: parent.pos + 1 + pos,
        };
        return false;
      }
    } else {
      return false;
    }
  });
  return foundNode;
}

export function lastNode(parent: NodeWithPos, predicate: (node: ProsemirrorNode) => boolean) {
  let last: NodeWithPos | undefined;
  parent.node.descendants((node, pos) => {
    if (predicate(node)) {
      last = {
        node,
        pos: parent.pos + 1 + pos,
      };
    }
  });
  return last;
}

export function nodeIsActive(state: EditorState, type: NodeType, attrs = {}) {
  const predicate = (n: ProsemirrorNode) => n.type === type;
  const node = findSelectedNodeOfType(type)(state.selection) || findParentNode(predicate)(state.selection);

  if (!Object.keys(attrs).length || !node) {
    return !!node;
  }

  return node.node.hasMarkup(type, attrs);
}

export function canInsertNode(state: EditorState, nodeType: NodeType) {
  const $from = state.selection.$from;
  for (let d = $from.depth; d >= 0; d--) {
    const index = $from.index(d);
    if ($from.node(d).canReplaceWith(index, index, nodeType)) {
      return true;
    }
  }
  return false;
}

export function insertAndSelectNode(view: EditorView, node: ProsemirrorNode) {
  // create new transaction
  const tr = view.state.tr;

  // insert the node over the existing selection
  tr.ensureMarks(node.marks);
  tr.replaceSelectionWith(node);

  // set selection to inserted node
  const selectionPos = tr.doc.resolve(tr.mapping.map(view.state.selection.from, -1));
  tr.setSelection(new NodeSelection(selectionPos));

  // dispatch transaction
  view.dispatch(tr);
}

export function editingRootNode(selection: Selection) {
  const schema = selection.$head.node().type.schema;
  return findParentNodeOfType(schema.nodes.body)(selection) || findParentNodeOfType(schema.nodes.note)(selection);
}

export function editingRootNodeClosestToPos($pos: ResolvedPos) {
  const schema = $pos.node().type.schema;<commit_msg>validate node selection in insertAndSelectNode<commit_after>/*
 * node.ts
 *
 * Copyright (C) 2020 by RStudio, PBC
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

import { Node as ProsemirrorNode, NodeSpec, NodeType, ResolvedPos } from 'prosemirror-model';
import { EditorState, Selection, NodeSelection } from 'prosemirror-state';
import {
  findParentNode,
  findSelectedNodeOfType,
  ContentNodeWithPos,
  NodeWithPos,
  findParentNodeOfType,
  findChildrenByType,
  findChildren,
  findParentNodeOfTypeClosestToPos,
} from 'prosemirror-utils';

import { EditorView } from 'prosemirror-view';

import {
  PandocTokenReader,
  PandocNodeWriterFn,
  PandocPreprocessorFn,
  PandocBlockReaderFn,
  PandocInlineHTMLReaderFn,
} from './pandoc';
import { PandocBlockCapsuleFilter } from './pandoc_capsule';

import { AttrEditOptions } from './attr_edit';
import { CommandFn } from './command';
import { ExecuteRmdChunkFn } from './rmd';

export interface PandocNode {
  readonly name: string;
  readonly spec: NodeSpec;
  readonly code_view?: CodeViewOptions;
  readonly attr_edit?: () => AttrEditOptions | null;
  readonly pandoc: {
    readonly readers?: readonly PandocTokenReader[];
    readonly writer?: PandocNodeWriterFn;
    readonly preprocessor?: PandocPreprocessorFn;
    readonly blockReader?: PandocBlockReaderFn;
    readonly inlineHTMLReader?: PandocInlineHTMLReaderFn;
    readonly blockCapsuleFilter?: PandocBlockCapsuleFilter;
  };
}

export interface CodeViewOptions {
  lang: (node: ProsemirrorNode, content: string) => string | null;
  attrEditFn?: CommandFn;
  executeRmdChunkFn?: ExecuteRmdChunkFn;
  classes?: string[];
  borderColorClass?: string;
  firstLineMeta?: boolean;
  lineNumbers?: boolean;
  bookdownTheorems?: boolean;
  lineNumberFormatter?: (lineNumber: number, lineCount?: number, line?: string) => string;
}

export type NodeTraversalFn = (
  node: Node,
  pos: number,
  parent: Node,
  index: number,
) => boolean | void | null | undefined;

export function findTopLevelBodyNodes(doc: ProsemirrorNode, predicate: (node: ProsemirrorNode) => boolean) {
  const body = findChildrenByType(doc, doc.type.schema.nodes.body, false)[0];
  const offset = body.pos + 1;
  const nodes = findChildren(body.node, predicate, false);
  return nodes.map(value => ({ ...value, pos: value.pos + offset }));
}

export function findNodeOfTypeInSelection(selection: Selection, type: NodeType): ContentNodeWithPos | undefined {
  return findSelectedNodeOfType(type)(selection) || findParentNode((n: ProsemirrorNode) => n.type === type)(selection);
}

export function firstNode(parent: NodeWithPos, predicate: (node: ProsemirrorNode) => boolean) {
  let foundNode: NodeWithPos | undefined;
  parent.node.descendants((node, pos) => {
    if (!foundNode) {
      if (predicate(node)) {
        foundNode = {
          node,
          pos: parent.pos + 1 + pos,
        };
        return false;
      }
    } else {
      return false;
    }
  });
  return foundNode;
}

export function lastNode(parent: NodeWithPos, predicate: (node: ProsemirrorNode) => boolean) {
  let last: NodeWithPos | undefined;
  parent.node.descendants((node, pos) => {
    if (predicate(node)) {
      last = {
        node,
        pos: parent.pos + 1 + pos,
      };
    }
  });
  return last;
}

export function nodeIsActive(state: EditorState, type: NodeType, attrs = {}) {
  const predicate = (n: ProsemirrorNode) => n.type === type;
  const node = findSelectedNodeOfType(type)(state.selection) || findParentNode(predicate)(state.selection);

  if (!Object.keys(attrs).length || !node) {
    return !!node;
  }

  return node.node.hasMarkup(type, attrs);
}

export function canInsertNode(state: EditorState, nodeType: NodeType) {
  const $from = state.selection.$from;
  for (let d = $from.depth; d >= 0; d--) {
    const index = $from.index(d);
    if ($from.node(d).canReplaceWith(index, index, nodeType)) {
      return true;
    }
  }
  return false;
}

export function insertAndSelectNode(view: EditorView, node: ProsemirrorNode) {
  // create new transaction
  const tr = view.state.tr;

  // insert the node over the existing selection
  tr.ensureMarks(node.marks);
  tr.replaceSelectionWith(node);

  // set selection to inserted node (or don't if our selection calculate was off,
  // as can happen when we insert into a list bullet)
  const selectionPos = tr.doc.resolve(tr.mapping.map(view.state.selection.from, -1));
  const selectionNode = tr.doc.nodeAt(selectionPos.pos);
  if (selectionNode && selectionNode.type === node.type) {
    tr.setSelection(new NodeSelection(selectionPos));
  }
 
  // dispatch transaction
  view.dispatch(tr);
}

export function editingRootNode(selection: Selection) {
  const schema = selection.$head.node().type.schema;
  return findParentNodeOfType(schema.nodes.body)(selection) || findParentNodeOfType(schema.nodes.note)(selection);
}

export function editingRootNodeClosestToPos($pos: ResolvedPos) {
  const schema = $pos.node().type.schema;
{'fraction_non_alphanumeric': 0.11555806087936867, 'fraction_numerical': 0.0024426907177752723, 'mean_word_length': 4.327827827827828, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6637089', 'n_tokens_mistral': 3265, 'n_tokens_neox': 3225, 'n_words': 862}