diff --git a/.eslintrc.js b/.eslintrc.js index 11e409fdd..05cb957b3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -250,7 +250,7 @@ module.exports = { "no-unneeded-ternary": "off", "no-unused-expressions": "off", "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "no-useless-backreference": "error", "no-useless-call": "off", "no-useless-computed-key": "error", diff --git a/rollup.config.js b/rollup.config.js index ef40aedfd..e4c07cdb5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,7 +19,7 @@ function commonPlugins() { }, ], }), - typescript(), + typescript({ noEmitOnError: false }), ] } diff --git a/src/js/editor/post/post-inserter.js b/src/js/editor/post/post-inserter.ts similarity index 71% rename from src/js/editor/post/post-inserter.js rename to src/js/editor/post/post-inserter.ts index 46bce5489..0b7e41e2a 100644 --- a/src/js/editor/post/post-inserter.js +++ b/src/js/editor/post/post-inserter.ts @@ -1,20 +1,32 @@ -import assert from 'mobiledoc-kit/utils/assert' -import { - MARKUP_SECTION_TYPE, - LIST_SECTION_TYPE, - POST_TYPE, - CARD_TYPE, - IMAGE_SECTION_TYPE, - LIST_ITEM_TYPE, -} from 'mobiledoc-kit/models/types' - -const MARKERABLE = 'markerable', - NESTED_MARKERABLE = 'nested_markerable', - NON_MARKERABLE = 'non_markerable' +import assert from '../../utils/assert' +import { Option } from '../../utils/types' +import { Type } from '../../models/types' +import Post from '../../models/post' +import Editor from '../editor' +import PostNodeBuilder from '../../models/post-node-builder' +import { Position } from '../../utils/cursor' +import Section, { WithParent } from '../../models/_section' +import MarkupSection from '../../models/markup-section' +import ListSection from '../../models/list-section' +import ListItem from '../../models/list-item' +import Card from '../../models/card' +import Image from '../../models/image' +import Markerable from '../../models/_markerable' +import { Cloneable } from '../../models/_cloneable' +import HasChildSections, { hasChildSections } from '../../models/_has-child-sections' + +const MARKERABLE = 'markerable' +const NESTED_MARKERABLE = 'nested_markerable' +const NON_MARKERABLE = 'non_markerable' class Visitor { - constructor(inserter, cursorPosition) { - let { postEditor, post } = inserter + postEditor: Editor + builder: PostNodeBuilder + _post: Post + _hasInsertedFirstLeafSection: boolean + _cursorPosition!: Position + + constructor({ postEditor, post }: Inserter, cursorPosition: Position) { this.postEditor = postEditor this._post = post this.cursorPosition = cursorPosition @@ -32,7 +44,7 @@ class Visitor { this.postEditor.setRange(position) } - visit(node) { + visit(node: Post | Section) { let method = node.type assert(`Cannot visit node of type ${node.type}`, !!this[method]) this[method](node) @@ -51,7 +63,7 @@ class Visitor { } get cursorSection() { - return this.cursorPosition.section + return this.cursorPosition.section! } get cursorOffset() { @@ -62,21 +74,22 @@ class Visitor { return this.cursorSection.isNested } - [POST_TYPE](node) { - if (this.cursorSection.isBlank && !this._isNested) { + [Type.POST](node: Post) { + let { cursorSection } = this + if (cursorSection.isBlank && !cursorSection.isNested) { // replace blank section with entire post let newSections = node.sections.map(s => s.clone()) - this._replaceSection(this.cursorSection, newSections) + this._replaceSection(cursorSection as Section & WithParent<HasChildSections>, newSections) } else { node.sections.forEach(section => this.visit(section)) } } - [MARKUP_SECTION_TYPE](node) { + [Type.MARKUP_SECTION](node: MarkupSection) { this[MARKERABLE](node) } - [LIST_SECTION_TYPE](node) { + [Type.LIST_SECTION](node: ListSection) { let hasNext = !!node.next node.items.forEach(item => this.visit(item)) @@ -85,19 +98,19 @@ class Visitor { } } - [LIST_ITEM_TYPE](node) { + [Type.LIST_ITEM](node: ListItem) { this[NESTED_MARKERABLE](node) } - [CARD_TYPE](node) { + [Type.CARD](node: Card) { this[NON_MARKERABLE](node) } - [IMAGE_SECTION_TYPE](node) { + [Type.IMAGE_SECTION](node: Image) { this[NON_MARKERABLE](node) } - [NON_MARKERABLE](section) { + [NON_MARKERABLE](section: Cloneable<Section>) { if (this._isNested) { this._breakNestedAtCursor() } else if (!this.cursorSection.isBlank) { @@ -107,7 +120,7 @@ class Visitor { this._insertLeafSection(section) } - [MARKERABLE](section) { + [MARKERABLE](section: Markerable) { if (this._canMergeSection(section)) { this._mergeSection(section) } else if (this._isNested && this._isMarkerable) { @@ -116,7 +129,7 @@ class Visitor { this._breakAtCursor() // Advance the cursor to the head of the blank list item - let nextPosition = this.cursorSection.next.headPosition() + let nextPosition = this.cursorSection.next!.headPosition() this.cursorPosition = nextPosition // Merge this section onto the list item @@ -127,22 +140,22 @@ class Visitor { } } - [NESTED_MARKERABLE](section) { + [NESTED_MARKERABLE](section: Markerable) { if (this._canMergeSection(section)) { this._mergeSection(section) return } - section = this._isNested ? section : this._wrapNestedSection(section) + let insertedSection = this._isNested ? section : this._wrapNestedSection(section as ListItem) this._breakAtCursor() - this._insertLeafSection(section) + this._insertLeafSection(insertedSection) } // break out of a nested cursor position _breakNestedAtCursor() { assert('Cannot call _breakNestedAtCursor if not nested', this._isNested) - let parent = this.cursorSection.parent + let parent = this.cursorSection.parent! let cursorAtEndOfList = this.cursorPosition.isEqual(parent.tailPosition()) if (cursorAtEndOfList) { @@ -160,6 +173,7 @@ class Visitor { let list = this.cursorSection.parent, position = this.cursorPosition, blank = this.builder.createMarkupSection() + let [pre, post] = this.postEditor._splitListAtPosition(list, position) let collection = this._post.sections, @@ -168,14 +182,14 @@ class Visitor { return [pre, blank, post] } - _wrapNestedSection(section) { - let tagName = section.parent.tagName + _wrapNestedSection(section: ListItem) { + let tagName = section.parent!.tagName let parent = this.builder.createListSection(tagName) parent.items.append(section.clone()) return parent } - _mergeSection(section) { + _mergeSection(section: Markerable) { assert('Can only merge markerable sections', this._isMarkerable && section.isMarkerable) this._hasInsertedFirstLeafSection = true @@ -213,8 +227,8 @@ class Visitor { this.cursorPosition = pre.tailPosition() } - _replaceSection(section, newSections) { - assert('Cannot replace section that does not have parent.sections', section.parent && section.parent.sections) + _replaceSection(section: Section, newSections: Section[]) { + assert('Cannot replace section that does not have parent.sections', hasChildSections(section.parent!)) assert('Must pass enumerable to _replaceSection', !!newSections.forEach) let collection = section.parent.sections @@ -228,7 +242,11 @@ class Visitor { this.cursorPosition = lastSection.tailPosition() } - _insertSectionBefore(section, reference) { + _insertSectionBefore(section: Section, reference?: Option<Section>) { + assert( + 'Cannot insert into section that does not have parent.sections', + hasChildSections(this.cursorSection.parent!) + ) let collection = this.cursorSection.parent.sections this.postEditor.insertSectionBefore(collection, section, reference) @@ -237,7 +255,7 @@ class Visitor { // Insert a section after the parent section. // E.g., add a markup section after a list section - _insertSectionAfter(section, parent) { + _insertSectionAfter(section: Section, parent: Section) { assert('Cannot _insertSectionAfter nested section', !parent.isNested) let reference = parent.next let collection = this._post.sections @@ -245,7 +263,7 @@ class Visitor { this.cursorPosition = section.tailPosition() } - _insertLeafSection(section) { + _insertLeafSection(section: Cloneable<Section>) { assert('Can only _insertLeafSection when cursor is at end of section', this.cursorPosition.isTail()) this._hasInsertedFirstLeafSection = true @@ -267,12 +285,15 @@ class Visitor { } export default class Inserter { - constructor(postEditor, post) { + postEditor: Editor + post: Post + + constructor(postEditor: Editor, post: Post) { this.postEditor = postEditor this.post = post } - insert(cursorPosition, newPost) { + insert(cursorPosition: Position, newPost: Post) { let visitor = new Visitor(this, cursorPosition) if (!newPost.isBlank) { visitor.visit(newPost) diff --git a/src/js/models/_cloneable.ts b/src/js/models/_cloneable.ts new file mode 100644 index 000000000..43dd18fa9 --- /dev/null +++ b/src/js/models/_cloneable.ts @@ -0,0 +1,13 @@ +import Section from './_section' + +export type Cloneable<T> = T & { + clone(): Cloneable<T> +} + +export function expectCloneable<T extends Section>(section: T): Cloneable<T> { + if (!('clone' in section)) { + throw new Error('Expected section to be cloneable') + } + + return section as Cloneable<T> +} diff --git a/src/js/models/_markerable.ts b/src/js/models/_markerable.ts index 101812e66..6f52e9025 100644 --- a/src/js/models/_markerable.ts +++ b/src/js/models/_markerable.ts @@ -8,14 +8,16 @@ import Section from './_section' import Marker from './marker' import { tagNameable } from './_tag-nameable' import { Type } from './types' +import { Cloneable } from './_cloneable' +import Markuperable from '../utils/markuperable' type MarkerableType = Type.LIST_ITEM | Type.MARKUP_SECTION -export default abstract class Markerable extends tagNameable(Section) { +export default abstract class Markerable extends tagNameable(Section) implements Cloneable<Markerable> { type: MarkerableType - markers: LinkedList<Marker> + markers: LinkedList<Markuperable> - constructor(type: MarkerableType, tagName: string, markers: Marker[] = []) { + constructor(type: MarkerableType, tagName: string, markers: Markuperable[] = []) { super(type) this.type = type this.isMarkerable = true @@ -70,7 +72,7 @@ export default abstract class Markerable extends tagNameable(Section) { * * @return {Number} The offset relative to the start of this section */ - offsetOfMarker(marker: Marker, markerOffset = 0) { + offsetOfMarker(marker: Markuperable, markerOffset = 0) { assert(`Cannot get offsetOfMarker for marker that is not child of this`, marker.section === this) // FIXME it is possible, when we get a cursor position before having finished reparsing, @@ -112,7 +114,7 @@ export default abstract class Markerable extends tagNameable(Section) { return [beforeSection, afterSection] } - abstract splitAtMarker(marker: Marker, offset: number): [Section, Section] + abstract splitAtMarker(marker: Markuperable, offset: number): [Section, Section] /** * Split this section's marker (if any) at the given offset, so that @@ -128,7 +130,7 @@ export default abstract class Markerable extends tagNameable(Section) { let markerOffset let len = 0 let currentMarker = this.markers.head - let edit: { added: Marker[]; removed: Marker[] } = { added: [], removed: [] } + let edit: { added: Markuperable[]; removed: Markuperable[] } = { added: [], removed: [] } if (!currentMarker) { let blankMarker = this.builder.createMarker() @@ -181,7 +183,7 @@ export default abstract class Markerable extends tagNameable(Section) { markerPositionAtOffset(offset: number) { let currentOffset = 0 - let currentMarker: Marker | null = null + let currentMarker: Markuperable | null = null let remaining = offset this.markers.detect(marker => { currentOffset = Math.min(remaining, marker.length) @@ -211,7 +213,7 @@ export default abstract class Markerable extends tagNameable(Section) { markersFor(headOffset: number, tailOffset: number) { const range = Range.create(this, headOffset, this, tailOffset) - let markers: Marker[] = [] + let markers: Markuperable[] = [] this._markersInRange(range, (marker, { markerHead, markerTail, isContained }) => { const cloned = marker.clone() if (!isContained) { @@ -237,7 +239,7 @@ export default abstract class Markerable extends tagNameable(Section) { // for each marker that is wholly or partially contained in the range. _markersInRange( range: Range, - callback: (marker: Marker, info: { markerHead: number; markerTail: number; isContained: boolean }) => void + callback: (marker: Markuperable, info: { markerHead: number; markerTail: number; isContained: boolean }) => void ) { const { head, tail } = range assert( @@ -274,7 +276,7 @@ export default abstract class Markerable extends tagNameable(Section) { // mutates this by appending the other section's (cloned) markers to it join(otherSection: Markerable) { let beforeMarker = this.markers.tail - let afterMarker: Marker | null = null + let afterMarker: Markuperable | null = null otherSection.markers.forEach(m => { if (!m.isBlank) { diff --git a/src/js/models/_section.ts b/src/js/models/_section.ts index accc29576..0bde1a7d5 100644 --- a/src/js/models/_section.ts +++ b/src/js/models/_section.ts @@ -1,13 +1,18 @@ import LinkedItem from '../utils/linked-item' import assert from '../utils/assert' +import { Option } from '../utils/types' import Position from '../utils/cursor/position' import Range from '../utils/cursor/range' -import Marker from './marker' import RenderNode from './render-node' import Post from './post' import { isListSection } from './is-list-section' import PostNodeBuilder from './post-node-builder' import { Type } from './types' +import Markuperable from '../utils/markuperable' + +export interface WithParent<T> { + parent: Option<T> +} export default class Section extends LinkedItem { type: Type @@ -20,10 +25,10 @@ export default class Section extends LinkedItem { isLeafSection = true isCardSection = false - post?: Post | null - renderNode: RenderNode | null = null + post?: Option<Post> + renderNode!: RenderNode - parent: Section | null = null + parent: Option<Section> = null builder!: PostNodeBuilder constructor(type: Type) { @@ -81,7 +86,7 @@ export default class Section extends LinkedItem { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars splitMarkerAtOffset(_offset: number) { - let blankEdit: { added: Marker[]; removed: Marker[] } = { added: [], removed: [] } + let blankEdit: { added: Markuperable[]; removed: Markuperable[] } = { added: [], removed: [] } return blankEdit } diff --git a/src/js/models/_tag-nameable.ts b/src/js/models/_tag-nameable.ts index d6d153f9a..a5ce323f3 100644 --- a/src/js/models/_tag-nameable.ts +++ b/src/js/models/_tag-nameable.ts @@ -28,3 +28,7 @@ export function tagNameable(Base: Constructor<Section>) { return TagNameable } + +export function isTagNameable(section: Section): section is Section & TagNameable { + return 'tagName' in section +} diff --git a/src/js/models/atom-node.ts b/src/js/models/atom-node.ts index 62ac3c5d7..68c8902c0 100644 --- a/src/js/models/atom-node.ts +++ b/src/js/models/atom-node.ts @@ -55,7 +55,7 @@ export default class AtomNode { return { name: this.atom.name, onTeardown: (callback: TeardownCallback) => (this._teardownCallback = callback), - save: (value: unknown, payload = {}) => { + save: (value: string, payload = {}) => { this.model.value = value this.model.payload = payload diff --git a/src/js/models/atom.ts b/src/js/models/atom.ts index 41101a26f..b3d226952 100644 --- a/src/js/models/atom.ts +++ b/src/js/models/atom.ts @@ -1,8 +1,8 @@ import { Type } from './types' import Markuperable from '../utils/markuperable' import assert from '../utils/assert' -import Marker from './marker' import Markup from './markup' +import PostNodeBuilder, { PostNode } from './post-node-builder' const ATOM_LENGTH = 1 @@ -11,14 +11,14 @@ export default class Atom extends Markuperable { isAtom = true name: string - value: unknown + value: string text: string payload: {} markups: Markup[] - builder: any + builder!: PostNodeBuilder - constructor(name: string, value: unknown, payload: {}, markups: Markup[] = []) { + constructor(name: string, value: string, payload: {}, markups: Markup[] = []) { super() this.name = name this.value = value @@ -55,7 +55,7 @@ export default class Atom extends Markuperable { } split(offset = 0, endOffset = offset) { - let markers: Marker[] = [] + let markers: Markuperable[] = [] if (endOffset === 0) { markers.push(this.builder.createMarker('', this.markups.slice())) @@ -70,13 +70,13 @@ export default class Atom extends Markuperable { return markers } - splitAtOffset(offset: number) { + splitAtOffset(offset: number): [Markuperable, Markuperable] { assert('Cannot split a marker at an offset > its length', offset <= this.length) let { builder } = this let clone = this.clone() let blankMarker = builder.createMarker('') - let pre: Marker, post: Marker + let pre: Markuperable, post: Markuperable if (offset === 0) { ;[pre, post] = [blankMarker, clone] @@ -93,3 +93,7 @@ export default class Atom extends Markuperable { return [pre, post] } } + +export function isAtom(postNode: PostNode): postNode is Atom { + return postNode.type === Type.ATOM +} diff --git a/src/js/models/image.ts b/src/js/models/image.ts index 3911ce66c..b54963522 100644 --- a/src/js/models/image.ts +++ b/src/js/models/image.ts @@ -9,6 +9,10 @@ export default class Image extends Section { super(Type.IMAGE_SECTION) } + clone() { + return this.builder.createImageSection(this.src) + } + canJoin() { return false } diff --git a/src/js/models/list-item.ts b/src/js/models/list-item.ts index bfd1674a0..32da2043b 100644 --- a/src/js/models/list-item.ts +++ b/src/js/models/list-item.ts @@ -4,7 +4,10 @@ import { normalizeTagName } from '../utils/dom-utils' import { contains } from '../utils/array-utils' import Section from './_section' import { expect } from '../utils/assert' +import { Option } from '../utils/types' import Marker from './marker' +import ListSection from './list-section' +import Markuperable from '../utils/markuperable' export const VALID_LIST_ITEM_TAGNAMES = ['li'].map(normalizeTagName) @@ -12,8 +15,9 @@ export default class ListItem extends Markerable { isListItem = true isNested = true section: Section | null = null + parent!: Option<ListSection> - constructor(tagName: string, markers: Marker[] = []) { + constructor(tagName: string, markers: Markuperable[] = []) { super(Type.LIST_ITEM, tagName, markers) } diff --git a/src/js/models/list-section.ts b/src/js/models/list-section.ts index b05667cd3..78d9c5d49 100644 --- a/src/js/models/list-section.ts +++ b/src/js/models/list-section.ts @@ -21,7 +21,7 @@ export default class ListSection extends attributable(tagNameable(Section)) impl items: LinkedList<ListItem> sections: LinkedList<ListItem> - constructor(tagName = DEFAULT_TAG_NAME, items = [], attributes = {}) { + constructor(tagName = DEFAULT_TAG_NAME, items: ListItem[] = [], attributes = {}) { super(LIST_SECTION_TYPE) this.tagName = tagName @@ -43,7 +43,7 @@ export default class ListSection extends attributable(tagNameable(Section)) impl return false } - isValidTagName(normalizedTagName) { + isValidTagName(normalizedTagName: string) { return contains(VALID_LIST_SECTION_TAGNAMES, normalizedTagName) } diff --git a/src/js/models/marker.ts b/src/js/models/marker.ts index 6b626248f..c771b9a61 100644 --- a/src/js/models/marker.ts +++ b/src/js/models/marker.ts @@ -3,8 +3,8 @@ import Markuperable from '../utils/markuperable' import assert from '../utils/assert' import { Type } from './types' import Markup from './markup' -import Markerable from './_markerable' import RenderNode from './render-node' +import PostNodeBuilder, { PostNode } from './post-node-builder' // Unicode uses a pair of "surrogate" characters" (a high- and low-surrogate) // to encode characters outside the basic multilingual plane (like emoji and @@ -22,10 +22,8 @@ export default class Marker extends Markuperable { value: string - builder: any + builder!: PostNodeBuilder markups: Markup[] = [] - section: Markerable | null = null - parent: Markerable | null = null renderNode: RenderNode | null = null constructor(value = '', markups: Markup[] = []) { @@ -48,10 +46,6 @@ export default class Marker extends Markuperable { return this.length === 0 } - charAt(offset: number) { - return this.value.slice(offset, offset + 1) - } - /** * A marker's text is equal to its value. * Compare with an Atom which distinguishes between text and value @@ -93,7 +87,7 @@ export default class Marker extends Markuperable { } split(offset = 0, endOffset = this.length) { - let markers = [ + let markers: [Marker, Marker, Marker] = [ this.builder.createMarker(this.value.substring(0, offset)), this.builder.createMarker(this.value.substring(offset, endOffset)), this.builder.createMarker(this.value.substring(endOffset)), @@ -106,7 +100,7 @@ export default class Marker extends Markuperable { /** * @return {Array} 2 markers either or both of which could be blank */ - splitAtOffset(offset: number) { + splitAtOffset(offset: number): [Marker, Marker] { assert('Cannot split a marker at an offset > its length', offset <= this.length) let { value, builder } = this @@ -121,3 +115,7 @@ export default class Marker extends Markuperable { return [pre, post] } } + +export function isMarker(postNode: PostNode): postNode is Marker { + return postNode.type === Type.MARKER +} diff --git a/src/js/models/markup-section.ts b/src/js/models/markup-section.ts index 832084ff1..ec00fb806 100644 --- a/src/js/models/markup-section.ts +++ b/src/js/models/markup-section.ts @@ -5,6 +5,8 @@ import { normalizeTagName } from '../utils/dom-utils' import { contains } from '../utils/array-utils' import { entries } from '../utils/object-utils' import Marker from './marker' +import Markuperable from '../utils/markuperable' +import Section from './_section' // valid values of `tagName` for a MarkupSection export const VALID_MARKUP_SECTION_TAGNAMES = ['aside', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'].map( @@ -23,7 +25,9 @@ export default class MarkupSection extends attributable(Markerable) { isMarkupSection = true isGenerated = false - constructor(tagName = DEFAULT_TAG_NAME, markers: Marker[] = [], attributes = {}) { + _inferredTagName: boolean = false + + constructor(tagName = DEFAULT_TAG_NAME, markers: Markuperable[] = [], attributes = {}) { super(MARKUP_SECTION_TYPE, tagName, markers) entries(attributes).forEach(([k, v]) => this.setAttribute(k, v)) } @@ -41,3 +45,11 @@ export default class MarkupSection extends attributable(Markerable) { return this._redistributeMarkers(beforeSection, afterSection, marker, offset) } } + +export function isMarkupSection(section: Section): section is MarkupSection { + return (section as MarkupSection).isMarkupSection +} + +export function hasInferredTagName(section: Section): section is MarkupSection { + return isMarkupSection(section) && section._inferredTagName +} diff --git a/src/js/models/post-node-builder.ts b/src/js/models/post-node-builder.ts index f3e39c210..6dc81d46a 100644 --- a/src/js/models/post-node-builder.ts +++ b/src/js/models/post-node-builder.ts @@ -15,6 +15,8 @@ import { DEFAULT_TAG_NAME as DEFAULT_LIST_SECTION_TAG_NAME } from './list-sectio import { normalizeTagName } from '../utils/dom-utils' import { objectToSortedKVArray } from '../utils/array-utils' import assert from '../utils/assert' +import Markuperable from '../utils/markuperable' +import Section from './_section' function cacheKey(tagName, attributes) { return `${normalizeTagName(tagName)}-${objectToSortedKVArray(attributes).join('-')}` @@ -52,14 +54,14 @@ export default class PostNodeBuilder { return post } - createMarkerableSection(type: Type.LIST_ITEM, tagName: string, markers: Marker[]): ListItem - createMarkerableSection(type: Type.MARKUP_SECTION, tagName: string, markers: Marker[]): MarkupSection + createMarkerableSection(type: Type.LIST_ITEM, tagName: string, markers: Markuperable[]): ListItem + createMarkerableSection(type: Type.MARKUP_SECTION, tagName: string, markers: Markuperable[]): MarkupSection createMarkerableSection( type: Exclude<Type, Type.LIST_ITEM & Type.MARKUP_SECTION>, tagName: string, - markers: Marker[] + markers: Markuperable[] ): never - createMarkerableSection(type: Type, tagName: string, markers: Marker[] = []) { + createMarkerableSection(type: Type, tagName: string, markers: Markuperable[] = []) { switch (type) { case LIST_ITEM_TYPE: return this.createListItem(markers) @@ -77,7 +79,7 @@ export default class PostNodeBuilder { */ createMarkupSection( tagName: string = DEFAULT_MARKUP_SECTION_TAG_NAME, - markers: Marker[] = [], + markers: Markuperable[] = [], isGenerated = false, attributes = {} ): MarkupSection { @@ -90,14 +92,14 @@ export default class PostNodeBuilder { return section } - createListSection(tagName = DEFAULT_LIST_SECTION_TAG_NAME, items = [], attributes = {}) { + createListSection(tagName = DEFAULT_LIST_SECTION_TAG_NAME, items: ListItem[] = [], attributes = {}) { tagName = normalizeTagName(tagName) const section = new ListSection(tagName, items, attributes) section.builder = this return section } - createListItem(markers: Marker[] = []) { + createListItem(markers: Markuperable[] = []) { const tagName = normalizeTagName('li') const item = new ListItem(tagName, markers) item.builder = this @@ -109,6 +111,7 @@ export default class PostNodeBuilder { if (url) { section.src = url } + section.builder = this return section } @@ -165,3 +168,5 @@ export default class PostNodeBuilder { return markup } } + +export type PostNode = Section | Markuperable | Marker diff --git a/src/js/models/post.ts b/src/js/models/post.ts index ccfe31c28..2099fc506 100644 --- a/src/js/models/post.ts +++ b/src/js/models/post.ts @@ -5,7 +5,6 @@ import Set from '../utils/set' import Position from '../utils/cursor/position' import Range from '../utils/cursor/range' import assert from '../utils/assert' -import Marker from './marker' import Markerable, { isMarkerable } from './_markerable' import Section from './_section' import PostNodeBuilder from './post-node-builder' @@ -14,6 +13,8 @@ import ListItem, { isListItem } from './list-item' import MarkupSection from './markup-section' import RenderNode from './render-node' import HasChildSections from './_has-child-sections' +import { expectCloneable, Cloneable } from './_cloneable' +import Markuperable from '../utils/markuperable' type SectionCallback = (section: Section, index: number) => void @@ -25,10 +26,10 @@ type SectionCallback = (section: Section, index: number) => void * When persisting a post, it must first be serialized (loss-lessly) into * mobiledoc using {@link Editor#serialize}. */ -export default class Post implements HasChildSections { +export default class Post implements HasChildSections<Cloneable<Section>> { type = Type.POST builder!: PostNodeBuilder - sections: LinkedList<Section> + sections: LinkedList<Cloneable<Section>> renderNode!: RenderNode constructor() { @@ -94,7 +95,7 @@ export default class Post implements HasChildSections { * @return {Array} markers that are completely contained by the range */ markersContainedByRange(range: Range): Array<any> { - const markers: Marker[] = [] + const markers: Markuperable[] = [] this.walkMarkerableSections(range, (section: Markerable) => { section._markersInRange(range.trimTo(section), (m, { isContained }) => { @@ -212,7 +213,7 @@ export default class Post implements HasChildSections { listParent: ListSection | null = null this.walkLeafSections(range, section => { - let newSection: Section + let newSection: ListItem | MarkupSection | Cloneable<Section> if (isMarkerable(section)) { if (isListItem(section)) { if (listParent) { @@ -250,15 +251,3 @@ export default class Post implements HasChildSections { return post } } - -interface Cloneable<T> { - clone(): T -} - -function expectCloneable<T extends Section>(section: T): T & Cloneable<T> { - if (!('clone' in section)) { - throw new Error('Expected section to be cloneable') - } - - return section as T & Cloneable<T> -} diff --git a/src/js/models/render-node.ts b/src/js/models/render-node.ts index d856513fc..18b974f27 100644 --- a/src/js/models/render-node.ts +++ b/src/js/models/render-node.ts @@ -8,8 +8,7 @@ import CardNode from './card-node' import AtomNode from './atom-node' import Section from './_section' import Markuperable from '../utils/markuperable' - -export type PostNode = Section | Markuperable +import { PostNode } from './post-node-builder' export default class RenderNode<T extends Node = Node> extends LinkedItem { parent: Option<RenderNode> = null diff --git a/src/js/models/render-tree.ts b/src/js/models/render-tree.ts index dcc575eaa..cd652ce25 100644 --- a/src/js/models/render-tree.ts +++ b/src/js/models/render-tree.ts @@ -1,6 +1,7 @@ -import RenderNode, { PostNode } from '../models/render-node' +import RenderNode from '../models/render-node' import ElementMap from '../utils/element-map' import Section from './_section' +import { PostNode } from './post-node-builder' export default class RenderTree { _rootNode: RenderNode diff --git a/src/js/parsers/dom.js b/src/js/parsers/dom.ts similarity index 62% rename from src/js/parsers/dom.js rename to src/js/parsers/dom.ts index a37a56401..eb5523be5 100644 --- a/src/js/parsers/dom.js +++ b/src/js/parsers/dom.ts @@ -1,43 +1,56 @@ import { NO_BREAK_SPACE, TAB_CHARACTER, ATOM_CLASS_NAME } from '../renderers/editor-dom' import { MARKUP_SECTION_TYPE, LIST_SECTION_TYPE, LIST_ITEM_TYPE } from '../models/types' -import { isTextNode, isCommentNode, isElementNode, getAttributes, normalizeTagName } from '../utils/dom-utils' -import { any, detect, forEach } from '../utils/array-utils' -import { TAB } from 'mobiledoc-kit/utils/characters' -import { ZWNJ } from 'mobiledoc-kit/renderers/editor-dom' - -import SectionParser from 'mobiledoc-kit/parsers/section' -import Markup from 'mobiledoc-kit/models/markup' +import { isTextNode, isElementNode, getAttributes, normalizeTagName } from '../utils/dom-utils' +import { any, detect, forEach, Indexable, ForEachable } from '../utils/array-utils' +import { TAB } from '../utils/characters' +import { ZWNJ } from '../renderers/editor-dom' +import SectionParser from '../parsers/section' +import Markup from '../models/markup' +import Markerable, { isMarkerable } from '../models/_markerable' +import PostNodeBuilder from '../models/post-node-builder' +import { Dict } from '../utils/types' +import Section from '../models/_section' +import Post from '../models/post' +import { Cloneable } from '../models/_cloneable' +import MarkupSection, { hasInferredTagName } from '../models/markup-section' +import RenderTree from '../models/render-tree' +import { isMarker } from '../models/marker' +import { isAtom } from '../models/atom' +import RenderNode from '../models/render-node' +import Markuperable from '../utils/markuperable' +import ListItem from '../models/list-item' +import ListSection from '../models/list-section' const GOOGLE_DOCS_CONTAINER_ID_REGEX = /^docs-internal-guid/ const NO_BREAK_SPACE_REGEX = new RegExp(NO_BREAK_SPACE, 'g') const TAB_CHARACTER_REGEX = new RegExp(TAB_CHARACTER, 'g') -export function transformHTMLText(textContent) { + +export function transformHTMLText(textContent: string) { let text = textContent text = text.replace(NO_BREAK_SPACE_REGEX, ' ') text = text.replace(TAB_CHARACTER_REGEX, TAB) return text } -export function trimSectionText(section) { - if (section.isMarkerable && section.markers.length) { +export function trimSectionText(section: Section) { + if (isMarkerable(section) && section.markers.length) { let { head, tail } = section.markers - head.value = head.value.replace(/^\s+/, '') - tail.value = tail.value.replace(/\s+$/, '') + head!.value = head!.value.replace(/^\s+/, '') + tail!.value = tail!.value.replace(/\s+$/, '') } } -function isGoogleDocsContainer(element) { +function isGoogleDocsContainer(element: Node) { return ( - !isTextNode(element) && - !isCommentNode(element) && + isElementNode(element) && normalizeTagName(element.tagName) === normalizeTagName('b') && GOOGLE_DOCS_CONTAINER_ID_REGEX.test(element.id) ) } -function detectRootElement(element) { - let childNodes = element.childNodes || [] +function detectRootElement(element: HTMLElement) { + let childNodes: Indexable<Node> = element.childNodes || [] let googleDocsContainer = detect(childNodes, isGoogleDocsContainer) if (googleDocsContainer) { @@ -47,23 +60,23 @@ function detectRootElement(element) { } } -const TAG_REMAPPING = { +const TAG_REMAPPING: Dict<string> = { b: 'strong', i: 'em', } -function remapTagName(tagName) { +function remapTagName(tagName: string) { let normalized = normalizeTagName(tagName) let remapped = TAG_REMAPPING[normalized] return remapped || normalized } -function trim(str) { +function trim(str: string) { return str.replace(/^\s+/, '').replace(/\s+$/, '') } -function walkMarkerableNodes(parent, callback) { - let currentNode = parent +function walkMarkerableNodes(parent: Node, callback: (node: Node) => void) { + let currentNode: Node | null = parent if (isTextNode(currentNode) || (isElementNode(currentNode) && currentNode.classList.contains(ATOM_CLASS_NAME))) { callback(currentNode) @@ -80,18 +93,21 @@ function walkMarkerableNodes(parent, callback) { * Parses DOM element -> Post * @private */ -class DOMParser { - constructor(builder, options = {}) { +export default class DOMParser { + builder: PostNodeBuilder + sectionParser: SectionParser + + constructor(builder: PostNodeBuilder, options = {}) { this.builder = builder this.sectionParser = new SectionParser(this.builder, options) } - parse(element) { + parse(element: HTMLElement) { const post = this.builder.createPost() let rootElement = detectRootElement(element) this._eachChildNode(rootElement, child => { - let sections = this.parseSections(child) + let sections = this.parseSections(child as HTMLElement) this.appendSections(post, sections) }) @@ -102,14 +118,14 @@ class DOMParser { return post } - appendSections(post, sections) { + appendSections(post: Post, sections: ForEachable<Cloneable<Section>>) { forEach(sections, section => this.appendSection(post, section)) } - appendSection(post, section) { + appendSection(post: Post, section: Cloneable<Section>) { if ( section.isBlank || - (section.isMarkerable && trim(section.text) === '' && !any(section.markers, marker => marker.isAtom)) + (isMarkerable(section) && trim(section.text) === '' && !any(section.markers, marker => marker.isAtom)) ) { return } @@ -117,8 +133,8 @@ class DOMParser { let lastSection = post.sections.tail if ( lastSection && - lastSection._inferredTagName && - section._inferredTagName && + hasInferredTagName(lastSection) && + hasInferredTagName(section) && lastSection.tagName === section.tagName ) { lastSection.join(section) @@ -127,19 +143,19 @@ class DOMParser { } } - _eachChildNode(element, callback) { + _eachChildNode(element: Node, callback: (element: Node) => void) { let nodes = isTextNode(element) ? [element] : element.childNodes forEach(nodes, node => callback(node)) } - parseSections(element) { + parseSections(element: HTMLElement) { return this.sectionParser.parse(element) } // walk up from the textNode until the rootNode, converting each // parentNode into a markup - collectMarkups(textNode, rootNode) { - let markups = [] + collectMarkups(textNode: Text, rootNode: Node) { + let markups: Markup[] = [] let currentNode = textNode.parentNode while (currentNode && currentNode !== rootNode) { let markup = this.markupFromNode(currentNode) @@ -153,8 +169,8 @@ class DOMParser { } // Turn an element node into a markup - markupFromNode(node) { - if (Markup.isValidElement(node)) { + markupFromNode(node: Node) { + if (isElementNode(node) && Markup.isValidElement(node)) { let tagName = remapTagName(node.tagName) let attributes = getAttributes(node) return this.builder.createMarkup(tagName, attributes) @@ -163,55 +179,55 @@ class DOMParser { // FIXME should move to the section parser? // FIXME the `collectMarkups` logic could simplify the section parser? - reparseSection(section, renderTree) { + reparseSection(section: Section, renderTree: RenderTree) { switch (section.type) { case LIST_SECTION_TYPE: - return this.reparseListSection(section, renderTree) + return this.reparseListSection(section as ListSection, renderTree) case LIST_ITEM_TYPE: - return this.reparseListItem(section, renderTree) + return this.reparseListItem(section as ListItem, renderTree) case MARKUP_SECTION_TYPE: - return this.reparseMarkupSection(section, renderTree) + return this.reparseMarkupSection(section as MarkupSection, renderTree) default: return // can only parse the above types } } - reparseMarkupSection(section, renderTree) { + reparseMarkupSection(section: MarkupSection, renderTree: RenderTree) { return this._reparseSectionContainingMarkers(section, renderTree) } - reparseListItem(listItem, renderTree) { + reparseListItem(listItem: ListItem, renderTree: RenderTree) { return this._reparseSectionContainingMarkers(listItem, renderTree) } - reparseListSection(listSection, renderTree) { + reparseListSection(listSection: ListSection, renderTree: RenderTree) { listSection.items.forEach(li => this.reparseListItem(li, renderTree)) } - _reparseSectionContainingMarkers(section, renderTree) { - let element = section.renderNode.element - let seenRenderNodes = [] - let previousMarker + _reparseSectionContainingMarkers(section: Markerable, renderTree: RenderTree) { + let element = section.renderNode.element! + let seenRenderNodes: RenderNode[] = [] + let previousMarker: Markuperable walkMarkerableNodes(element, node => { - let marker + let marker!: Markuperable let renderNode = renderTree.getElementRenderNode(node) if (renderNode) { - if (renderNode.postNode.isMarker) { - let text = transformHTMLText(node.textContent) - let markups = this.collectMarkups(node, element) + if (isMarker(renderNode.postNode!)) { + let text = transformHTMLText(node.textContent || '') + let markups = this.collectMarkups(node as Text, element) if (text.length) { - marker = renderNode.postNode + marker = renderNode.postNode! marker.value = text marker.markups = markups } else { renderNode.scheduleForRemoval() } - } else if (renderNode.postNode.isAtom) { + } else if (isAtom(renderNode.postNode!)) { let { headTextNode, tailTextNode } = renderNode - if (headTextNode.textContent !== ZWNJ) { - let value = headTextNode.textContent.replace(new RegExp(ZWNJ, 'g'), '') - headTextNode.textContent = ZWNJ + if (headTextNode!.textContent !== ZWNJ) { + let value = headTextNode!.textContent!.replace(new RegExp(ZWNJ, 'g'), '') + headTextNode!.textContent = ZWNJ if (previousMarker && previousMarker.isMarker) { previousMarker.value += value if (previousMarker.renderNode) { @@ -231,16 +247,16 @@ class DOMParser { section.renderNode.childNodes.insertBefore(newPreviousRenderNode, renderNode) } } - if (tailTextNode.textContent !== ZWNJ) { - let value = tailTextNode.textContent.replace(new RegExp(ZWNJ, 'g'), '') - tailTextNode.textContent = ZWNJ + if (tailTextNode!.textContent !== ZWNJ) { + let value = tailTextNode!.textContent!.replace(new RegExp(ZWNJ, 'g'), '') + tailTextNode!.textContent = ZWNJ if (renderNode.postNode.next && renderNode.postNode.next.isMarker) { let nextMarker = renderNode.postNode.next if (nextMarker.renderNode) { - let nextValue = nextMarker.renderNode.element.textContent - nextMarker.renderNode.element.textContent = value + nextValue + let nextValue = nextMarker.renderNode.element!.textContent + nextMarker.renderNode.element!.textContent = value + nextValue } else { let nextValue = value + nextMarker.value nextMarker.value = nextValue @@ -266,7 +282,7 @@ class DOMParser { } } } else if (isTextNode(node)) { - let text = transformHTMLText(node.textContent) + let text = transformHTMLText(node.textContent!) let markups = this.collectMarkups(node, element) marker = this.builder.createMarker(text, markups) @@ -295,5 +311,3 @@ class DOMParser { } } } - -export default DOMParser diff --git a/src/js/parsers/html.js b/src/js/parsers/html.ts similarity index 68% rename from src/js/parsers/html.js rename to src/js/parsers/html.ts index 20cb92b43..2a77cd1d6 100644 --- a/src/js/parsers/html.js +++ b/src/js/parsers/html.ts @@ -1,9 +1,14 @@ import { parseHTML } from '../utils/dom-utils' import assert from '../utils/assert' import DOMParser from './dom' +import PostNodeBuilder from '../models/post-node-builder' +import Post from '../models/post' export default class HTMLParser { - constructor(builder, options = {}) { + builder: PostNodeBuilder + options: {} + + constructor(builder: PostNodeBuilder, options = {}) { assert('Must pass builder to HTMLParser', builder) this.builder = builder this.options = options @@ -13,7 +18,7 @@ export default class HTMLParser { * @param {String} html to parse * @return {Post} A post abstract */ - parse(html) { + parse(html: string): Post { let dom = parseHTML(html) let parser = new DOMParser(this.builder, this.options) return parser.parse(dom) diff --git a/src/js/parsers/mobiledoc/0-2.js b/src/js/parsers/mobiledoc/0-2.ts similarity index 55% rename from src/js/parsers/mobiledoc/0-2.js rename to src/js/parsers/mobiledoc/0-2.ts index 3410c7f00..4a8620dfb 100644 --- a/src/js/parsers/mobiledoc/0-2.js +++ b/src/js/parsers/mobiledoc/0-2.ts @@ -1,17 +1,31 @@ import { - MOBILEDOC_MARKUP_SECTION_TYPE, - MOBILEDOC_IMAGE_SECTION_TYPE, - MOBILEDOC_LIST_SECTION_TYPE, - MOBILEDOC_CARD_SECTION_TYPE, -} from 'mobiledoc-kit/renderers/mobiledoc/0-2' -import { kvArrayToObject, filter } from '../../utils/array-utils' -import assert from 'mobiledoc-kit/utils/assert' + MobiledocMarkerType, + MobiledocV0_2, + MobiledocSection, + MobiledocMarker, + MobiledocCardSection, + MobiledocImageSection, + MobiledocMarkupSection, + MobiledocListSection, +} from '../../renderers/mobiledoc/0-2' +import { MobiledocSectionKind } from '../../renderers/mobiledoc/constants' +import { kvArrayToObject, filter, ForEachable } from '../../utils/array-utils' +import assert from '../../utils/assert' +import PostNodeBuilder from '../../models/post-node-builder' +import Post from '../../models/post' +import Markup from '../../models/markup' +import ListSection from '../../models/list-section' +import Markerable from '../../models/_markerable' /* * Parses from mobiledoc -> post */ export default class MobiledocParser { - constructor(builder) { + builder: PostNodeBuilder + markups!: Markup[] + markerTypes!: Markup[] + + constructor(builder: PostNodeBuilder) { this.builder = builder } @@ -19,7 +33,7 @@ export default class MobiledocParser { * @param {Mobiledoc} * @return {Post} */ - parse({ sections: sectionData }) { + parse({ sections: sectionData }: MobiledocV0_2): Post { try { const markerTypes = sectionData[0] const sections = sectionData[1] @@ -36,50 +50,49 @@ export default class MobiledocParser { } } - parseMarkerTypes(markerTypes) { + parseMarkerTypes(markerTypes: MobiledocMarkerType[]) { return markerTypes.map(markerType => this.parseMarkerType(markerType)) } - parseMarkerType([tagName, attributesArray]) { + parseMarkerType([tagName, attributesArray]: MobiledocMarkerType) { const attributesObject = kvArrayToObject(attributesArray || []) return this.builder.createMarkup(tagName, attributesObject) } - parseSections(sections, post) { + parseSections(sections: ForEachable<MobiledocSection>, post: Post) { sections.forEach(section => this.parseSection(section, post)) } - parseSection(section, post) { - let [type] = section - switch (type) { - case MOBILEDOC_MARKUP_SECTION_TYPE: + parseSection(section: MobiledocSection, post: Post) { + switch (section[0]) { + case MobiledocSectionKind.MARKUP: this.parseMarkupSection(section, post) break - case MOBILEDOC_IMAGE_SECTION_TYPE: + case MobiledocSectionKind.IMAGE: this.parseImageSection(section, post) break - case MOBILEDOC_CARD_SECTION_TYPE: + case MobiledocSectionKind.CARD: this.parseCardSection(section, post) break - case MOBILEDOC_LIST_SECTION_TYPE: + case MobiledocSectionKind.LIST: this.parseListSection(section, post) break default: - assert(`Unexpected section type ${type}`, false) + assert(`Unexpected section type ${section[0]}`, false) } } - parseCardSection([, name, payload], post) { + parseCardSection([, name, payload]: MobiledocCardSection, post: Post) { const section = this.builder.createCardSection(name, payload) post.sections.append(section) } - parseImageSection([, src], post) { + parseImageSection([, src]: MobiledocImageSection, post: Post) { const section = this.builder.createImageSection(src) post.sections.append(section) } - parseMarkupSection([, tagName, markers], post) { + parseMarkupSection([, tagName, markers]: MobiledocMarkupSection, post: Post) { const section = this.builder.createMarkupSection(tagName.toLowerCase() === 'pull-quote' ? 'aside' : tagName) post.sections.append(section) this.parseMarkers(markers, section) @@ -90,27 +103,27 @@ export default class MobiledocParser { }) } - parseListSection([, tagName, items], post) { + parseListSection([, tagName, items]: MobiledocListSection, post: Post) { const section = this.builder.createListSection(tagName) post.sections.append(section) this.parseListItems(items, section) } - parseListItems(items, section) { + parseListItems(items: MobiledocMarker[][], section: ListSection) { items.forEach(i => this.parseListItem(i, section)) } - parseListItem(markers, section) { + parseListItem(markers: MobiledocMarker[], section: ListSection) { const item = this.builder.createListItem() this.parseMarkers(markers, item) section.items.append(item) } - parseMarkers(markers, parent) { + parseMarkers(markers: MobiledocMarker[], parent: Markerable) { markers.forEach(m => this.parseMarker(m, parent)) } - parseMarker([markerTypeIndexes, closeCount, value], parent) { + parseMarker([markerTypeIndexes, closeCount, value]: [number[], number, string], parent: Markerable) { markerTypeIndexes.forEach(index => { this.markups.push(this.markerTypes[index]) }) diff --git a/src/js/parsers/mobiledoc/0-3-1.js b/src/js/parsers/mobiledoc/0-3-1.ts similarity index 55% rename from src/js/parsers/mobiledoc/0-3-1.js rename to src/js/parsers/mobiledoc/0-3-1.ts index 80c810767..197327267 100644 --- a/src/js/parsers/mobiledoc/0-3-1.js +++ b/src/js/parsers/mobiledoc/0-3-1.ts @@ -1,19 +1,35 @@ import { - MOBILEDOC_MARKUP_SECTION_TYPE, - MOBILEDOC_IMAGE_SECTION_TYPE, - MOBILEDOC_LIST_SECTION_TYPE, - MOBILEDOC_CARD_SECTION_TYPE, - MOBILEDOC_MARKUP_MARKER_TYPE, - MOBILEDOC_ATOM_MARKER_TYPE, -} from 'mobiledoc-kit/renderers/mobiledoc/0-3-1' -import { kvArrayToObject, filter } from '../../utils/array-utils' -import assert from 'mobiledoc-kit/utils/assert' + MobiledocMarkerType, + MobiledocMarkupSection, + MobiledocSection, + MobiledocCard, + MobiledocAtom, + MobiledocCardSection, + MobiledocImageSection, + MobiledocListSection, + MobiledocMarker, +} from '../../renderers/mobiledoc/0-3' +import { kvArrayToObject, filter, ForEachable } from '../../utils/array-utils' +import assert from '../../utils/assert' +import { MobiledocMarkerKind, MobiledocSectionKind } from '../../renderers/mobiledoc/constants' +import PostNodeBuilder from '../../models/post-node-builder' +import { MobiledocV0_3_1 } from '../../renderers/mobiledoc/0-3-1' +import Markup from '../../models/markup' +import Post from '../../models/post' +import ListSection from '../../models/list-section' +import Markerable from '../../models/_markerable' /* * Parses from mobiledoc -> post */ export default class MobiledocParser { - constructor(builder) { + builder: PostNodeBuilder + markups!: Markup[] + markerTypes!: Markup[] + cardTypes!: MobiledocCard[] + atomTypes!: MobiledocAtom[] + + constructor(builder: PostNodeBuilder) { this.builder = builder } @@ -21,7 +37,7 @@ export default class MobiledocParser { * @param {Mobiledoc} * @return {Post} */ - parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }) { + parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }: MobiledocV0_3_1): Post { try { const post = this.builder.createPost() @@ -37,79 +53,78 @@ export default class MobiledocParser { } } - parseMarkerTypes(markerTypes) { + parseMarkerTypes(markerTypes: MobiledocMarkerType[]) { return markerTypes.map(markerType => this.parseMarkerType(markerType)) } - parseMarkerType([tagName, attributesArray]) { + parseMarkerType([tagName, attributesArray]: MobiledocMarkerType) { const attributesObject = kvArrayToObject(attributesArray || []) return this.builder.createMarkup(tagName, attributesObject) } - parseCardTypes(cardTypes) { + parseCardTypes(cardTypes: MobiledocCard[]) { return cardTypes.map(cardType => this.parseCardType(cardType)) } - parseCardType([cardName, cardPayload]) { + parseCardType([cardName, cardPayload]: MobiledocCard): MobiledocCard { return [cardName, cardPayload] } - parseAtomTypes(atomTypes) { + parseAtomTypes(atomTypes: MobiledocAtom[]) { return atomTypes.map(atomType => this.parseAtomType(atomType)) } - parseAtomType([atomName, atomValue, atomPayload]) { + parseAtomType([atomName, atomValue, atomPayload]: MobiledocAtom): MobiledocAtom { return [atomName, atomValue, atomPayload] } - parseSections(sections, post) { + parseSections(sections: ForEachable<MobiledocSection>, post: Post) { sections.forEach(section => this.parseSection(section, post)) } - parseSection(section, post) { - let [type] = section - switch (type) { - case MOBILEDOC_MARKUP_SECTION_TYPE: + parseSection(section: MobiledocSection, post: Post) { + switch (section[0]) { + case MobiledocSectionKind.MARKUP: this.parseMarkupSection(section, post) break - case MOBILEDOC_IMAGE_SECTION_TYPE: + case MobiledocSectionKind.IMAGE: this.parseImageSection(section, post) break - case MOBILEDOC_CARD_SECTION_TYPE: + case MobiledocSectionKind.CARD: this.parseCardSection(section, post) break - case MOBILEDOC_LIST_SECTION_TYPE: + case MobiledocSectionKind.LIST: this.parseListSection(section, post) break default: - assert('Unexpected section type ${type}', false) + assert(`Unexpected section type ${section[0]}`, false) } } - getAtomTypeFromIndex(index) { + getAtomTypeFromIndex(index: number) { const atomType = this.atomTypes[index] assert(`No atom definition found at index ${index}`, !!atomType) return atomType } - getCardTypeFromIndex(index) { + getCardTypeFromIndex(index: number) { const cardType = this.cardTypes[index] assert(`No card definition found at index ${index}`, !!cardType) return cardType } - parseCardSection([, cardIndex], post) { + parseCardSection([, cardIndex]: MobiledocCardSection, post: Post) { const [name, payload] = this.getCardTypeFromIndex(cardIndex) const section = this.builder.createCardSection(name, payload) post.sections.append(section) } - parseImageSection([, src], post) { + parseImageSection([, src]: MobiledocImageSection, post: Post) { const section = this.builder.createImageSection(src) post.sections.append(section) } - parseMarkupSection([, tagName, markers], post) { + parseMarkupSection([, tagName, markers]: MobiledocMarkupSection, post: Post) { const section = this.builder.createMarkupSection(tagName) post.sections.append(section) this.parseMarkers(markers, section) @@ -120,27 +135,27 @@ export default class MobiledocParser { }) } - parseListSection([, tagName, items], post) { + parseListSection([, tagName, items]: MobiledocListSection, post: Post) { const section = this.builder.createListSection(tagName) post.sections.append(section) this.parseListItems(items, section) } - parseListItems(items, section) { + parseListItems(items: MobiledocMarker[][], section: ListSection) { items.forEach(i => this.parseListItem(i, section)) } - parseListItem(markers, section) { + parseListItem(markers: MobiledocMarker[], section: ListSection) { const item = this.builder.createListItem() this.parseMarkers(markers, item) section.items.append(item) } - parseMarkers(markers, parent) { + parseMarkers(markers: MobiledocMarker[], parent: Markerable) { markers.forEach(m => this.parseMarker(m, parent)) } - parseMarker([type, markerTypeIndexes, closeCount, value], parent) { + parseMarker([type, markerTypeIndexes, closeCount, value]: MobiledocMarker, parent: Markerable) { markerTypeIndexes.forEach(index => { this.markups.push(this.markerTypes[index]) }) @@ -151,12 +166,12 @@ export default class MobiledocParser { this.markups = this.markups.slice(0, this.markups.length - closeCount) } - buildMarkerType(type, value) { + buildMarkerType(type: MobiledocMarkerKind, value: string | number) { switch (type) { - case MOBILEDOC_MARKUP_MARKER_TYPE: - return this.builder.createMarker(value, this.markups.slice()) - case MOBILEDOC_ATOM_MARKER_TYPE: { - const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value) + case MobiledocMarkerKind.MARKUP: + return this.builder.createMarker(value as string, this.markups.slice()) + case MobiledocMarkerKind.ATOM: { + const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value as number) return this.builder.createAtom(atomName, atomValue, atomPayload, this.markups.slice()) } default: diff --git a/src/js/parsers/mobiledoc/0-3-2.js b/src/js/parsers/mobiledoc/0-3-2.ts similarity index 57% rename from src/js/parsers/mobiledoc/0-3-2.js rename to src/js/parsers/mobiledoc/0-3-2.ts index 1b5d6e74d..3518b8a43 100644 --- a/src/js/parsers/mobiledoc/0-3-2.js +++ b/src/js/parsers/mobiledoc/0-3-2.ts @@ -1,21 +1,41 @@ import { - MOBILEDOC_MARKUP_SECTION_TYPE, - MOBILEDOC_IMAGE_SECTION_TYPE, - MOBILEDOC_LIST_SECTION_TYPE, - MOBILEDOC_CARD_SECTION_TYPE, - MOBILEDOC_MARKUP_MARKER_TYPE, - MOBILEDOC_ATOM_MARKER_TYPE, -} from '../../renderers/mobiledoc/0-3-2' - -import { kvArrayToObject, filter } from '../../utils/array-utils' + MobiledocMarkerType, + MobiledocCard, + MobiledocAtom, + MobiledocMarker, + MobiledocCardSection, + MobiledocImageSection, + MobiledocMarkupSection, + MobiledocListSection, +} from '../../renderers/mobiledoc/0-3' + +import { kvArrayToObject, filter, ForEachable } from '../../utils/array-utils' import assert from '../../utils/assert' import { entries } from '../../utils/object-utils' +import Markup from '../../models/markup' +import PostNodeBuilder from '../../models/post-node-builder' +import { + MobiledocV0_3_2, + MobiledocAttributedMarkupSection, + MobiledocAttributedListSection, + MobiledocAttributedSection, +} from '../../renderers/mobiledoc/0-3-2' +import Post from '../../models/post' +import { MobiledocSectionKind, MobiledocMarkerKind } from '../../renderers/mobiledoc/constants' +import ListSection from '../../models/list-section' +import Markerable from '../../models/_markerable' /* * Parses from mobiledoc -> post */ export default class MobiledocParser { - constructor(builder) { + builder: PostNodeBuilder + markups!: Markup[] + markerTypes!: Markup[] + cardTypes!: MobiledocCard[] + atomTypes!: MobiledocAtom[] + + constructor(builder: PostNodeBuilder) { this.builder = builder } @@ -23,7 +43,7 @@ export default class MobiledocParser { * @param {Mobiledoc} * @return {Post} */ - parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }) { + parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }: MobiledocV0_3_2): Post { try { const post = this.builder.createPost() @@ -39,79 +59,81 @@ export default class MobiledocParser { } } - parseMarkerTypes(markerTypes) { + parseMarkerTypes(markerTypes: MobiledocMarkerType[]) { return markerTypes.map(markerType => this.parseMarkerType(markerType)) } - parseMarkerType([tagName, attributesArray]) { + parseMarkerType([tagName, attributesArray]: MobiledocMarkerType) { const attributesObject = kvArrayToObject(attributesArray || []) return this.builder.createMarkup(tagName, attributesObject) } - parseCardTypes(cardTypes) { + parseCardTypes(cardTypes: MobiledocCard[]) { return cardTypes.map(cardType => this.parseCardType(cardType)) } - parseCardType([cardName, cardPayload]) { + parseCardType([cardName, cardPayload]: MobiledocCard): MobiledocCard { return [cardName, cardPayload] } - parseAtomTypes(atomTypes) { + parseAtomTypes(atomTypes: MobiledocAtom[]) { return atomTypes.map(atomType => this.parseAtomType(atomType)) } - parseAtomType([atomName, atomValue, atomPayload]) { + parseAtomType([atomName, atomValue, atomPayload]: MobiledocAtom): MobiledocAtom { return [atomName, atomValue, atomPayload] } - parseSections(sections, post) { + parseSections(sections: ForEachable<MobiledocAttributedSection>, post: Post) { sections.forEach(section => this.parseSection(section, post)) } - parseSection(section, post) { - let [type] = section - switch (type) { - case MOBILEDOC_MARKUP_SECTION_TYPE: + parseSection(section: MobiledocAttributedSection, post: Post) { + switch (section[0]) { + case MobiledocSectionKind.MARKUP: this.parseMarkupSection(section, post) break - case MOBILEDOC_IMAGE_SECTION_TYPE: + case MobiledocSectionKind.IMAGE: this.parseImageSection(section, post) break - case MOBILEDOC_CARD_SECTION_TYPE: + case MobiledocSectionKind.CARD: this.parseCardSection(section, post) break - case MOBILEDOC_LIST_SECTION_TYPE: + case MobiledocSectionKind.LIST: this.parseListSection(section, post) break default: - assert('Unexpected section type ${type}', false) + assert(`Unexpected section type ${section[0]}`, false) } } - getAtomTypeFromIndex(index) { + getAtomTypeFromIndex(index: number) { const atomType = this.atomTypes[index] assert(`No atom definition found at index ${index}`, !!atomType) return atomType } - getCardTypeFromIndex(index) { + getCardTypeFromIndex(index: number) { const cardType = this.cardTypes[index] assert(`No card definition found at index ${index}`, !!cardType) return cardType } - parseCardSection([, cardIndex], post) { + parseCardSection([, cardIndex]: MobiledocCardSection, post: Post) { const [name, payload] = this.getCardTypeFromIndex(cardIndex) const section = this.builder.createCardSection(name, payload) post.sections.append(section) } - parseImageSection([, src], post) { + parseImageSection([, src]: MobiledocImageSection, post: Post) { const section = this.builder.createImageSection(src) post.sections.append(section) } - parseMarkupSection([, tagName, markers, attributesArray], post) { + parseMarkupSection( + [, tagName, markers, attributesArray]: MobiledocMarkupSection | MobiledocAttributedMarkupSection, + post: Post + ) { const section = this.builder.createMarkupSection(tagName) post.sections.append(section) if (attributesArray) { @@ -127,7 +149,10 @@ export default class MobiledocParser { }) } - parseListSection([, tagName, items, attributesArray], post) { + parseListSection( + [, tagName, items, attributesArray]: MobiledocListSection | MobiledocAttributedListSection, + post: Post + ) { const section = this.builder.createListSection(tagName) post.sections.append(section) if (attributesArray) { @@ -138,21 +163,21 @@ export default class MobiledocParser { this.parseListItems(items, section) } - parseListItems(items, section) { + parseListItems(items: MobiledocMarker[][], section: ListSection) { items.forEach(i => this.parseListItem(i, section)) } - parseListItem(markers, section) { + parseListItem(markers: MobiledocMarker[], section: ListSection) { const item = this.builder.createListItem() this.parseMarkers(markers, item) section.items.append(item) } - parseMarkers(markers, parent) { + parseMarkers(markers: MobiledocMarker[], parent: Markerable) { markers.forEach(m => this.parseMarker(m, parent)) } - parseMarker([type, markerTypeIndexes, closeCount, value], parent) { + parseMarker([type, markerTypeIndexes, closeCount, value]: MobiledocMarker, parent: Markerable) { markerTypeIndexes.forEach(index => { this.markups.push(this.markerTypes[index]) }) @@ -163,12 +188,12 @@ export default class MobiledocParser { this.markups = this.markups.slice(0, this.markups.length - closeCount) } - buildMarkerType(type, value) { + buildMarkerType(type: MobiledocMarkerKind, value: string | number) { switch (type) { - case MOBILEDOC_MARKUP_MARKER_TYPE: - return this.builder.createMarker(value, this.markups.slice()) - case MOBILEDOC_ATOM_MARKER_TYPE: { - const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value) + case MobiledocMarkerKind.MARKUP: + return this.builder.createMarker(value as string, this.markups.slice()) + case MobiledocMarkerKind.ATOM: { + const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value as number) return this.builder.createAtom(atomName, atomValue, atomPayload, this.markups.slice()) } default: diff --git a/src/js/parsers/mobiledoc/0-3.js b/src/js/parsers/mobiledoc/0-3.ts similarity index 55% rename from src/js/parsers/mobiledoc/0-3.js rename to src/js/parsers/mobiledoc/0-3.ts index 03b6f00c1..3f29a17d6 100644 --- a/src/js/parsers/mobiledoc/0-3.js +++ b/src/js/parsers/mobiledoc/0-3.ts @@ -1,19 +1,35 @@ import { - MOBILEDOC_MARKUP_SECTION_TYPE, - MOBILEDOC_IMAGE_SECTION_TYPE, - MOBILEDOC_LIST_SECTION_TYPE, - MOBILEDOC_CARD_SECTION_TYPE, - MOBILEDOC_MARKUP_MARKER_TYPE, - MOBILEDOC_ATOM_MARKER_TYPE, -} from 'mobiledoc-kit/renderers/mobiledoc/0-3' -import { kvArrayToObject, filter } from '../../utils/array-utils' -import assert from 'mobiledoc-kit/utils/assert' + MobiledocMarkerType, + MobiledocMarkupSection, + MobiledocSection, + MobiledocCard, + MobiledocAtom, + MobiledocV0_3, + MobiledocMarker, + MobiledocCardSection, + MobiledocImageSection, + MobiledocListSection, +} from '../../renderers/mobiledoc/0-3' +import { kvArrayToObject, filter, ForEachable } from '../../utils/array-utils' +import assert from '../../utils/assert' +import PostNodeBuilder from '../../models/post-node-builder' +import Markup from '../../models/markup' +import Post from '../../models/post' +import { MobiledocSectionKind, MobiledocMarkerKind } from '../../renderers/mobiledoc/constants' +import ListSection from '../../models/list-section' +import Markerable from '../../models/_markerable' /* * Parses from mobiledoc -> post */ export default class MobiledocParser { - constructor(builder) { + builder: PostNodeBuilder + markups!: Markup[] + markerTypes!: Markup[] + cardTypes!: MobiledocCard[] + atomTypes!: MobiledocAtom[] + + constructor(builder: PostNodeBuilder) { this.builder = builder } @@ -21,7 +37,7 @@ export default class MobiledocParser { * @param {Mobiledoc} * @return {Post} */ - parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }) { + parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }: MobiledocV0_3): Post { try { const post = this.builder.createPost() @@ -37,79 +53,78 @@ export default class MobiledocParser { } } - parseMarkerTypes(markerTypes) { + parseMarkerTypes(markerTypes: MobiledocMarkerType[]) { return markerTypes.map(markerType => this.parseMarkerType(markerType)) } - parseMarkerType([tagName, attributesArray]) { + parseMarkerType([tagName, attributesArray]: MobiledocMarkerType) { const attributesObject = kvArrayToObject(attributesArray || []) return this.builder.createMarkup(tagName, attributesObject) } - parseCardTypes(cardTypes) { + parseCardTypes(cardTypes: MobiledocCard[]) { return cardTypes.map(cardType => this.parseCardType(cardType)) } - parseCardType([cardName, cardPayload]) { + parseCardType([cardName, cardPayload]: MobiledocCard): MobiledocCard { return [cardName, cardPayload] } - parseAtomTypes(atomTypes) { + parseAtomTypes(atomTypes: MobiledocAtom[]) { return atomTypes.map(atomType => this.parseAtomType(atomType)) } - parseAtomType([atomName, atomValue, atomPayload]) { + parseAtomType([atomName, atomValue, atomPayload]: MobiledocAtom): MobiledocAtom { return [atomName, atomValue, atomPayload] } - parseSections(sections, post) { + parseSections(sections: ForEachable<MobiledocSection>, post: Post) { sections.forEach(section => this.parseSection(section, post)) } - parseSection(section, post) { - let [type] = section - switch (type) { - case MOBILEDOC_MARKUP_SECTION_TYPE: + parseSection(section: MobiledocSection, post: Post) { + switch (section[0]) { + case MobiledocSectionKind.MARKUP: this.parseMarkupSection(section, post) break - case MOBILEDOC_IMAGE_SECTION_TYPE: + case MobiledocSectionKind.IMAGE: this.parseImageSection(section, post) break - case MOBILEDOC_CARD_SECTION_TYPE: + case MobiledocSectionKind.CARD: this.parseCardSection(section, post) break - case MOBILEDOC_LIST_SECTION_TYPE: + case MobiledocSectionKind.LIST: this.parseListSection(section, post) break default: - assert('Unexpected section type ${type}', false) + assert(`Unexpected section type ${section[0]}`, false) } } - getAtomTypeFromIndex(index) { + getAtomTypeFromIndex(index: number) { const atomType = this.atomTypes[index] assert(`No atom definition found at index ${index}`, !!atomType) return atomType } - getCardTypeFromIndex(index) { + getCardTypeFromIndex(index: number) { const cardType = this.cardTypes[index] assert(`No card definition found at index ${index}`, !!cardType) return cardType } - parseCardSection([, cardIndex], post) { + parseCardSection([, cardIndex]: MobiledocCardSection, post: Post) { const [name, payload] = this.getCardTypeFromIndex(cardIndex) const section = this.builder.createCardSection(name, payload) post.sections.append(section) } - parseImageSection([, src], post) { + parseImageSection([, src]: MobiledocImageSection, post: Post) { const section = this.builder.createImageSection(src) post.sections.append(section) } - parseMarkupSection([, tagName, markers], post) { + parseMarkupSection([, tagName, markers]: MobiledocMarkupSection, post: Post) { const section = this.builder.createMarkupSection(tagName.toLowerCase() === 'pull-quote' ? 'aside' : tagName) post.sections.append(section) this.parseMarkers(markers, section) @@ -120,27 +135,27 @@ export default class MobiledocParser { }) } - parseListSection([, tagName, items], post) { + parseListSection([, tagName, items]: MobiledocListSection, post: Post) { const section = this.builder.createListSection(tagName) post.sections.append(section) this.parseListItems(items, section) } - parseListItems(items, section) { + parseListItems(items: MobiledocMarker[][], section: ListSection) { items.forEach(i => this.parseListItem(i, section)) } - parseListItem(markers, section) { + parseListItem(markers: MobiledocMarker[], section: ListSection) { const item = this.builder.createListItem() this.parseMarkers(markers, item) section.items.append(item) } - parseMarkers(markers, parent) { + parseMarkers(markers: MobiledocMarker[], parent: Markerable) { markers.forEach(m => this.parseMarker(m, parent)) } - parseMarker([type, markerTypeIndexes, closeCount, value], parent) { + parseMarker([type, markerTypeIndexes, closeCount, value]: MobiledocMarker, parent: Markerable) { markerTypeIndexes.forEach(index => { this.markups.push(this.markerTypes[index]) }) @@ -151,12 +166,12 @@ export default class MobiledocParser { this.markups = this.markups.slice(0, this.markups.length - closeCount) } - buildMarkerType(type, value) { + buildMarkerType(type: MobiledocMarkerKind, value: string | number) { switch (type) { - case MOBILEDOC_MARKUP_MARKER_TYPE: - return this.builder.createMarker(value, this.markups.slice()) - case MOBILEDOC_ATOM_MARKER_TYPE: { - const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value) + case MobiledocMarkerKind.MARKUP: + return this.builder.createMarker(value as string, this.markups.slice()) + case MobiledocMarkerKind.ATOM: { + const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value as number) return this.builder.createAtom(atomName, atomValue, atomPayload, this.markups.slice()) } default: diff --git a/src/js/parsers/mobiledoc/index.js b/src/js/parsers/mobiledoc/index.js deleted file mode 100644 index 50194df92..000000000 --- a/src/js/parsers/mobiledoc/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import MobiledocParser_0_2 from './0-2' -import MobiledocParser_0_3 from './0-3' -import MobiledocParser_0_3_1 from './0-3-1' -import MobiledocParser_0_3_2 from './0-3-2' - -import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-2' -import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3 } from 'mobiledoc-kit/renderers/mobiledoc/0-3' -import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_1 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-1' -import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-2' -import assert from 'mobiledoc-kit/utils/assert' - -function parseVersion(mobiledoc) { - return mobiledoc.version -} - -export default { - parse(builder, mobiledoc) { - let version = parseVersion(mobiledoc) - switch (version) { - case MOBILEDOC_VERSION_0_2: - return new MobiledocParser_0_2(builder).parse(mobiledoc) - case MOBILEDOC_VERSION_0_3: - return new MobiledocParser_0_3(builder).parse(mobiledoc) - case MOBILEDOC_VERSION_0_3_1: - return new MobiledocParser_0_3_1(builder).parse(mobiledoc) - case MOBILEDOC_VERSION_0_3_2: - return new MobiledocParser_0_3_2(builder).parse(mobiledoc) - default: - assert(`Unknown version of mobiledoc parser requested: ${version}`, false) - } - }, -} diff --git a/src/js/parsers/mobiledoc/index.ts b/src/js/parsers/mobiledoc/index.ts new file mode 100644 index 000000000..f81d302b2 --- /dev/null +++ b/src/js/parsers/mobiledoc/index.ts @@ -0,0 +1,31 @@ +import MobiledocParser_0_2 from './0-2' +import MobiledocParser_0_3 from './0-3' +import MobiledocParser_0_3_1 from './0-3-1' +import MobiledocParser_0_3_2 from './0-3-2' + +import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2, MobiledocV0_2 } from '../../renderers/mobiledoc/0-2' +import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3, MobiledocV0_3 } from '../../renderers/mobiledoc/0-3' +import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_1, MobiledocV0_3_1 } from '../../renderers/mobiledoc/0-3-1' +import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_2, MobiledocV0_3_2 } from '../../renderers/mobiledoc/0-3-2' +import assert from '../../utils/assert' +import PostNodeBuilder from '../../models/post-node-builder' +import Post from '../../models/post' + +type Mobiledoc = MobiledocV0_2 | MobiledocV0_3 | MobiledocV0_3_1 | MobiledocV0_3_2 + +export default { + parse(builder: PostNodeBuilder, mobiledoc: Mobiledoc): Post { + switch (mobiledoc.version) { + case MOBILEDOC_VERSION_0_2: + return new MobiledocParser_0_2(builder).parse(mobiledoc) + case MOBILEDOC_VERSION_0_3: + return new MobiledocParser_0_3(builder).parse(mobiledoc) + case MOBILEDOC_VERSION_0_3_1: + return new MobiledocParser_0_3_1(builder).parse(mobiledoc) + case MOBILEDOC_VERSION_0_3_2: + return new MobiledocParser_0_3_2(builder).parse(mobiledoc) + default: + assert(`Unknown version of mobiledoc parser requested: ${(mobiledoc as any).version}`, false) + } + }, +} diff --git a/src/js/parsers/section.js b/src/js/parsers/section.ts similarity index 68% rename from src/js/parsers/section.js rename to src/js/parsers/section.ts index b606d9c62..cb271d564 100644 --- a/src/js/parsers/section.js +++ b/src/js/parsers/section.ts @@ -1,17 +1,33 @@ -import { DEFAULT_TAG_NAME, VALID_MARKUP_SECTION_TAGNAMES } from 'mobiledoc-kit/models/markup-section' -import { VALID_LIST_SECTION_TAGNAMES } from 'mobiledoc-kit/models/list-section' -import { VALID_LIST_ITEM_TAGNAMES } from 'mobiledoc-kit/models/list-item' -import { LIST_SECTION_TYPE, LIST_ITEM_TYPE, MARKUP_SECTION_TYPE } from 'mobiledoc-kit/models/types' -import { VALID_MARKUP_TAGNAMES } from 'mobiledoc-kit/models/markup' -import { getAttributes, normalizeTagName, isTextNode, isCommentNode, NODE_TYPES } from '../utils/dom-utils' -import { any, forEach, contains } from 'mobiledoc-kit/utils/array-utils' +import MarkupSection, { + DEFAULT_TAG_NAME, + VALID_MARKUP_SECTION_TAGNAMES, + isMarkupSection as sectionIsMarkupSection, +} from '../models/markup-section' +import { VALID_LIST_SECTION_TAGNAMES, isListSection as sectionIsListSection } from '../models/list-section' +import { VALID_LIST_ITEM_TAGNAMES, isListItem as sectionIsListItem } from '../models/list-item' +import { LIST_SECTION_TYPE, LIST_ITEM_TYPE, MARKUP_SECTION_TYPE } from '../models/types' +import Markup, { VALID_MARKUP_TAGNAMES } from '../models/markup' +import { + getAttributes, + normalizeTagName, + isTextNode, + isCommentNode, + NODE_TYPES, + isElementNode, +} from '../utils/dom-utils' +import { any, forEach, contains } from '../utils/array-utils' import { transformHTMLText, trimSectionText } from '../parsers/dom' -import assert from '../utils/assert' +import assert, { assertType, expect } from '../utils/assert' +import PostNodeBuilder from '../models/post-node-builder' +import Section from '../models/_section' +import Marker from '../models/marker' +import Markerable, { isMarkerable } from '../models/_markerable' +import { Cloneable } from '../models/_cloneable' const SKIPPABLE_ELEMENT_TAG_NAMES = ['style', 'head', 'title', 'meta'].map(normalizeTagName) const NEWLINES = /\s*\n\s*/g -function sanitize(text) { +function sanitize(text: string) { return text.replace(NEWLINES, ' ') } @@ -20,13 +36,40 @@ function sanitize(text) { * elements contained within * @private */ -class SectionParser { - constructor(builder, options = {}) { + +interface SectionParserOptions { + plugins?: SectionParserPlugin[] +} + +interface SectionParserState { + section?: Cloneable<Section> | null + text?: string + markups?: Markup[] +} + +interface SectionParseEnv { + addSection: (section: Cloneable<Section>) => void + addMarkerable: (marker: Marker) => void + nodeFinished(): void +} + +export type SectionParserPlugin = (node: Node, builder: PostNodeBuilder, env: SectionParseEnv) => void + +type SectionParserNode = HTMLElement | Text | Comment + +export default class SectionParser { + builder: PostNodeBuilder + plugins: SectionParserPlugin[] + + sections!: Cloneable<Section>[] + state!: SectionParserState + + constructor(builder: PostNodeBuilder, options: SectionParserOptions = {}) { this.builder = builder this.plugins = options.plugins || [] } - parse(element) { + parse(element: HTMLElement) { if (this._isSkippable(element)) { return [] } @@ -47,7 +90,7 @@ class SectionParser { let childNodes = isTextNode(element) ? [element] : element.childNodes forEach(childNodes, el => { - this.parseNode(el) + this.parseNode(el as SectionParserNode) }) } @@ -56,20 +99,20 @@ class SectionParser { return this.sections } - runPlugins(node) { + runPlugins(node: Node) { let isNodeFinished = false let env = { - addSection: section => { + addSection: (section: Cloneable<Section>) => { // avoid creating empty paragraphs due to wrapper elements around // parser-plugin-handled elements - if (this.state.section && this.state.section.isMarkerable && !this.state.section.text && !this.state.text) { + if (this.state.section && isMarkerable(this.state.section) && !this.state.section.text && !this.state.text) { this.state.section = null } else { this._closeCurrentSection() } this.sections.push(section) }, - addMarkerable: marker => { + addMarkerable: (marker: Marker) => { let { state } = this let { section } = state // if the first element doesn't create it's own state and it's plugin @@ -79,8 +122,9 @@ class SectionParser { state.section = this.builder.createMarkupSection(normalizeTagName('p')) section = state.section } - assert( + assertType<Markerable>( 'Markerables can only be appended to markup sections and list item sections', + section, section && section.isMarkerable ) if (state.text) { @@ -103,7 +147,7 @@ class SectionParser { } /* eslint-disable complexity */ - parseNode(node) { + parseNode(node: SectionParserNode) { if (!this.state.section) { this._updateStateFromElement(node) } @@ -115,7 +159,7 @@ class SectionParser { // handle closing the current section and starting a new one if we hit a // new-section-creating element. - if (this.state.section && !isTextNode(node) && node.tagName) { + if (this.state.section && isElementNode(node) && node.tagName) { let tagName = normalizeTagName(node.tagName) let isListSection = contains(VALID_LIST_SECTION_TAGNAMES, tagName) let isListItem = contains(VALID_LIST_ITEM_TAGNAMES, tagName) @@ -125,16 +169,16 @@ class SectionParser { // lists can continue after breaking out for a markup section, // in that situation, start a new list using the same list type - if (isListItem && this.state.section.isMarkupSection) { + if (isListItem && sectionIsMarkupSection(this.state.section)) { this._closeCurrentSection() - this._updateStateFromElement(node.parentElement) + this._updateStateFromElement(node.parentElement!) } // we can hit a list item after parsing a nested list, when that happens // and the lists are of different types we need to make sure we switch // the list type back - if (isListItem && lastSection && lastSection.isListSection) { - let parentElement = node.parentElement + if (isListItem && lastSection && sectionIsListSection(lastSection)) { + let parentElement = expect(node.parentElement, 'expected node to have parent element') let parentElementTagName = normalizeTagName(parentElement.tagName) if (parentElementTagName !== lastSection.tagName) { this._closeCurrentSection() @@ -152,14 +196,14 @@ class SectionParser { !lastSection.isListSection ) { this._closeCurrentSection() - this._updateStateFromElement(node.parentElement) + this._updateStateFromElement(node.parentElement!) } // if we have consecutive list sections of different types (ul, ol) then // ensure we close the current section and start a new one let isNewListSection = lastSection && - lastSection.isListSection && + sectionIsListSection(lastSection) && this.state.section.isListItem && isListSection && tagName !== lastSection.tagName @@ -171,7 +215,10 @@ class SectionParser { this.state.section.isListItem && tagName === 'p' && !node.nextSibling && - contains(VALID_LIST_ITEM_TAGNAMES, normalizeTagName(node.parentElement.tagName)) + contains( + VALID_LIST_ITEM_TAGNAMES, + normalizeTagName(expect(node.parentElement, 'expected node to have parent element').tagName) + ) ) { this.parseElementNode(node) return @@ -179,7 +226,7 @@ class SectionParser { // avoid creating empty paragraphs due to wrapper elements around // section-creating elements - if (this.state.section.isMarkerable && !this.state.text && this.state.section.markers.length === 0) { + if (isMarkerable(this.state.section) && !this.state.text && this.state.section.markers.length === 0) { this.state.section = null } else { this._closeCurrentSection() @@ -188,13 +235,13 @@ class SectionParser { this._updateStateFromElement(node) } - if (this.state.section.isListSection) { + if (this.state.section && this.state.section.isListSection) { // ensure the list section is closed and added to the sections list. // _closeCurrentSection handles pushing list items onto the list section this._closeCurrentSection() forEach(node.childNodes, node => { - this.parseNode(node) + this.parseNode(node as SectionParserNode) }) return } @@ -202,28 +249,29 @@ class SectionParser { switch (node.nodeType) { case NODE_TYPES.TEXT: - this.parseTextNode(node) + this.parseTextNode(node as Text) break case NODE_TYPES.ELEMENT: - this.parseElementNode(node) + this.parseElementNode(node as HTMLElement) break } } - parseElementNode(element) { + parseElementNode(element: HTMLElement) { let { state } = this + assert('expected markups to be non-null', state.markups) const markups = this._markupsFromElement(element) - if (markups.length && state.text.length && state.section.isMarkerable) { + if (markups.length && state.text!.length && isMarkerable(state.section!)) { this._createMarker() } state.markups.push(...markups) forEach(element.childNodes, node => { - this.parseNode(node) + this.parseNode(node as SectionParserNode) }) - if (markups.length && state.text.length && state.section.isMarkerable) { + if (markups.length && state.text!.length && state.section!.isMarkerable) { // create the marker started for this node this._createMarker() } @@ -232,12 +280,12 @@ class SectionParser { state.markups.splice(-markups.length, markups.length) } - parseTextNode(textNode) { + parseTextNode(textNode: Text) { let { state } = this - state.text += sanitize(textNode.textContent) + state.text += sanitize(textNode.textContent!) } - _updateStateFromElement(element) { + _updateStateFromElement(element: SectionParserNode) { if (isCommentNode(element)) { return } @@ -257,18 +305,18 @@ class SectionParser { } // close a trailing text node if it exists - if (state.text.length && state.section.isMarkerable) { + if (state.text!.length && state.section.isMarkerable) { this._createMarker() } // push listItems onto the listSection or add a new section - if (state.section.isListItem && lastSection && lastSection.isListSection) { + if (sectionIsListItem(state.section) && lastSection && sectionIsListSection(lastSection)) { trimSectionText(state.section) lastSection.items.append(state.section) } else { // avoid creating empty markup sections, especially useful for indented source if ( - state.section.isMarkerable && + isMarkerable(state.section) && !state.section.text.trim() && !any(state.section.markers, marker => marker.isAtom) ) { @@ -278,7 +326,7 @@ class SectionParser { } // remove empty list sections before creating a new section - if (lastSection && lastSection.isListSection && lastSection.items.length === 0) { + if (lastSection && sectionIsListSection(lastSection) && lastSection.items.length === 0) { sections.pop() } @@ -289,9 +337,9 @@ class SectionParser { state.text = '' } - _markupsFromElement(element) { + _markupsFromElement(element: HTMLElement | Text) { let { builder } = this - let markups = [] + let markups: Markup[] = [] if (isTextNode(element)) { return markups } @@ -306,7 +354,7 @@ class SectionParser { return markups } - _isValidMarkupForElement(tagName, element) { + _isValidMarkupForElement(tagName: string, element: HTMLElement) { if (VALID_MARKUP_TAGNAMES.indexOf(tagName) === -1) { return false } else if (tagName === 'b') { @@ -317,9 +365,9 @@ class SectionParser { return true } - _markupsFromElementStyle(element) { + _markupsFromElementStyle(element: HTMLElement) { let { builder } = this - let markups = [] + let markups: Markup[] = [] let { fontStyle, fontWeight } = element.style if (fontStyle === 'italic') { markups.push(builder.createMarkup('em')) @@ -332,15 +380,16 @@ class SectionParser { _createMarker() { let { state } = this - let text = transformHTMLText(state.text) + let text = transformHTMLText(state.text!) let marker = this.builder.createMarker(text, state.markups) + assertType<Markerable>('expected section to be markerable', state.section, isMarkerable(state.section!)) state.section.markers.append(marker) state.text = '' } - _getSectionDetails(element) { - let sectionType, - tagName, + _getSectionDetails(element: HTMLElement | Text) { + let sectionType: string, + tagName: string, inferredTagName = false if (isTextNode(element)) { @@ -376,13 +425,13 @@ class SectionParser { return { sectionType, tagName, inferredTagName } } - _createSectionFromElement(element) { + _createSectionFromElement(element: Comment | HTMLElement) { if (isCommentNode(element)) { return } let { builder } = this - let section + let section: Cloneable<Section> let { tagName, sectionType, inferredTagName } = this._getSectionDetails(element) switch (sectionType) { @@ -394,7 +443,7 @@ class SectionParser { break case MARKUP_SECTION_TYPE: section = builder.createMarkupSection(tagName) - section._inferredTagName = inferredTagName + ;(section as MarkupSection)._inferredTagName = inferredTagName break default: assert('Cannot parse section from element', false) @@ -403,12 +452,7 @@ class SectionParser { return section } - _isSkippable(element) { - return ( - element.nodeType === NODE_TYPES.ELEMENT && - contains(SKIPPABLE_ELEMENT_TAG_NAMES, normalizeTagName(element.tagName)) - ) + _isSkippable(element: Node) { + return isElementNode(element) && contains(SKIPPABLE_ELEMENT_TAG_NAMES, normalizeTagName(element.tagName)) } } - -export default SectionParser diff --git a/src/js/parsers/text.js b/src/js/parsers/text.ts similarity index 61% rename from src/js/parsers/text.js rename to src/js/parsers/text.ts index 65cbe72ad..60d05a9fb 100644 --- a/src/js/parsers/text.js +++ b/src/js/parsers/text.ts @@ -1,6 +1,12 @@ -import assert from 'mobiledoc-kit/utils/assert' -import { MARKUP_SECTION_TYPE, LIST_SECTION_TYPE } from 'mobiledoc-kit/models/types' -import { DEFAULT_TAG_NAME as DEFAULT_MARKUP_SECTION_TAG_NAME } from 'mobiledoc-kit/models/markup-section' +import assert from '../utils/assert' +import { MARKUP_SECTION_TYPE, LIST_SECTION_TYPE } from '../models/types' +import { DEFAULT_TAG_NAME as DEFAULT_MARKUP_SECTION_TAG_NAME } from '../models/markup-section' +import PostNodeBuilder from '../models/post-node-builder' +import Post from '../models/post' +import Section from '../models/_section' +import { Option } from '../utils/types' +import ListSection, { isListSection } from '../models/list-section' +import { Cloneable } from '../models/_cloneable' const UL_LI_REGEX = /^\* (.*)$/ const OL_LI_REGEX = /^\d\.? (.*)$/ @@ -15,8 +21,16 @@ function normalizeLineEndings(text) { return text.replace(CR_LF_REGEX, LF).replace(CR_REGEX, LF) } +export interface TextParserOptions {} + export default class TextParser { - constructor(builder, options) { + builder: PostNodeBuilder + options: TextParserOptions + post: Post + + prevSection: Option<Cloneable<Section>> + + constructor(builder: PostNodeBuilder, options: TextParserOptions) { this.builder = builder this.options = options @@ -28,7 +42,7 @@ export default class TextParser { * @param {String} text to parse * @return {Post} a post abstract */ - parse(text) { + parse(text: string): Post { text = normalizeLineEndings(text) text.split(SECTION_BREAK).forEach(text => { let section = this._parseSection(text) @@ -38,7 +52,7 @@ export default class TextParser { return this.post } - _parseSection(text) { + _parseSection(text: string) { let tagName = DEFAULT_MARKUP_SECTION_TAG_NAME, type = MARKUP_SECTION_TYPE, section @@ -46,11 +60,11 @@ export default class TextParser { if (UL_LI_REGEX.test(text)) { tagName = 'ul' type = LIST_SECTION_TYPE - text = text.match(UL_LI_REGEX)[1] + text = text.match(UL_LI_REGEX)![1] } else if (OL_LI_REGEX.test(text)) { tagName = 'ol' type = LIST_SECTION_TYPE - text = text.match(OL_LI_REGEX)[1] + text = text.match(OL_LI_REGEX)![1] } let markers = [this.builder.createMarker(text)] @@ -72,19 +86,19 @@ export default class TextParser { return section } - _appendSection(section) { + _appendSection(section: Cloneable<Section>) { let isSameListSection = - section.isListSection && + isListSection(section) && this.prevSection && - this.prevSection.isListSection && + isListSection(this.prevSection) && this.prevSection.tagName === section.tagName if (isSameListSection) { - section.items.forEach(item => { - this.prevSection.items.append(item.clone()) + ;(section as ListSection).items.forEach(item => { + ;(this.prevSection as ListSection).items.append(item.clone()) }) } else { - this.post.sections.insertAfter(section, this.prevSection) + this.post.sections.insertAfter(section, this.prevSection!) this.prevSection = section } } diff --git a/src/js/renderers/editor-dom.ts b/src/js/renderers/editor-dom.ts index 84758d595..0cb9e70d1 100644 --- a/src/js/renderers/editor-dom.ts +++ b/src/js/renderers/editor-dom.ts @@ -13,7 +13,7 @@ import Section from '../models/_section' import { Attributable } from '../models/_attributable' import { TagNameable } from '../models/_tag-nameable' import ListSection from '../models/list-section' -import RenderNode, { PostNode } from '../models/render-node' +import RenderNode from '../models/render-node' import { Option, Maybe } from '../utils/types' import Atom from '../models/atom' import Editor from '../editor/editor' @@ -23,6 +23,7 @@ import ListItem from '../models/list-item' import Image from '../models/image' import Card from '../models/card' import RenderTree from '../models/render-tree' +import { PostNode } from '../models/post-node-builder' export const CARD_ELEMENT_CLASS_NAME = '__mobiledoc-card' export const NO_BREAK_SPACE = '\u00A0' diff --git a/src/js/renderers/mobiledoc/0-2.ts b/src/js/renderers/mobiledoc/0-2.ts index aa450fbb4..7d2b096d0 100644 --- a/src/js/renderers/mobiledoc/0-2.ts +++ b/src/js/renderers/mobiledoc/0-2.ts @@ -9,12 +9,9 @@ import Image from '../../models/image' import Card from '../../models/card' import Marker from '../../models/marker' import Markup from '../../models/markup' +import { MobiledocSectionKind } from './constants' export const MOBILEDOC_VERSION = '0.2.0' -export const MOBILEDOC_MARKUP_SECTION_TYPE = 1 -export const MOBILEDOC_IMAGE_SECTION_TYPE = 2 -export const MOBILEDOC_LIST_SECTION_TYPE = 3 -export const MOBILEDOC_CARD_SECTION_TYPE = 10 const visitor = { [Type.POST](node: Post, opcodes: Opcodes) { @@ -48,38 +45,43 @@ const visitor = { }, } -type OpcodeCompilerMarker = [number[], number, unknown] -type OpcodeCompilerSection = - | [typeof MOBILEDOC_MARKUP_SECTION_TYPE, string, OpcodeCompilerMarker[]] - | [typeof MOBILEDOC_LIST_SECTION_TYPE, string, OpcodeCompilerMarker[][]] - | [typeof MOBILEDOC_IMAGE_SECTION_TYPE, string] - | [typeof MOBILEDOC_CARD_SECTION_TYPE, string, {}] +export type MobiledocMarker = [number[], number, string] +export type MobiledocMarkerType = [string, string[]?] -type PostOpcodeCompilerMarkerType = [string, string[]?] +export type MobiledocMarkupSection = [MobiledocSectionKind.MARKUP, string, MobiledocMarker[]] +export type MobiledocListSection = [MobiledocSectionKind.LIST, string, MobiledocMarker[][]] +export type MobiledocImageSection = [MobiledocSectionKind.IMAGE, string] +export type MobiledocCardSection = [MobiledocSectionKind.CARD, string, {}] + +export type MobiledocSection = + | MobiledocMarkupSection + | MobiledocListSection + | MobiledocImageSection + | MobiledocCardSection class PostOpcodeCompiler { markupMarkerIds!: number[] - markers!: OpcodeCompilerMarker[] - sections!: OpcodeCompilerSection[] - items!: OpcodeCompilerMarker[][] - markerTypes!: PostOpcodeCompilerMarkerType[] + markers!: MobiledocMarker[] + sections!: MobiledocSection[] + items!: MobiledocMarker[][] + markerTypes!: MobiledocMarkerType[] result!: MobiledocV0_2 _markerTypeCache!: { [key: string]: number } - openMarker(closeCount: number, value: unknown) { + openMarker(closeCount: number, value: string) { this.markupMarkerIds = [] this.markers.push([this.markupMarkerIds, closeCount, value || '']) } openMarkupSection(tagName: string) { this.markers = [] - this.sections.push([MOBILEDOC_MARKUP_SECTION_TYPE, tagName, this.markers]) + this.sections.push([MobiledocSectionKind.MARKUP, tagName, this.markers]) } openListSection(tagName: string) { this.items = [] - this.sections.push([MOBILEDOC_LIST_SECTION_TYPE, tagName, this.items]) + this.sections.push([MobiledocSectionKind.LIST, tagName, this.items]) } openListItem() { @@ -88,11 +90,11 @@ class PostOpcodeCompiler { } openImageSection(url: string) { - this.sections.push([MOBILEDOC_IMAGE_SECTION_TYPE, url]) + this.sections.push([MobiledocSectionKind.IMAGE, url]) } openCardSection(name: string, payload: {}) { - this.sections.push([MOBILEDOC_CARD_SECTION_TYPE, name, payload]) + this.sections.push([MobiledocSectionKind.CARD, name, payload]) } openPost() { @@ -117,7 +119,7 @@ class PostOpcodeCompiler { let index = this._markerTypeCache[key] if (index === undefined) { - let markerType: PostOpcodeCompilerMarkerType = [tagName] + let markerType: MobiledocMarkerType = [tagName] if (attributesArray.length) { markerType.push(attributesArray) } @@ -133,7 +135,7 @@ class PostOpcodeCompiler { export interface MobiledocV0_2 { version: typeof MOBILEDOC_VERSION - sections: [PostOpcodeCompilerMarkerType[], OpcodeCompilerSection[]] + sections: [MobiledocMarkerType[], MobiledocSection[]] } /** diff --git a/src/js/renderers/mobiledoc/0-3-1.ts b/src/js/renderers/mobiledoc/0-3-1.ts index 42f7b3297..be31307f0 100644 --- a/src/js/renderers/mobiledoc/0-3-1.ts +++ b/src/js/renderers/mobiledoc/0-3-1.ts @@ -11,15 +11,10 @@ import Marker from '../../models/marker' import Markup from '../../models/markup' import Atom from '../../models/atom' import { Dict } from '../../utils/types' +import { MobiledocSectionKind, MobiledocMarkerKind } from './constants' +import { MobiledocMarker, MobiledocSection, MobiledocMarkerType, MobiledocAtom, MobiledocCard } from './0-3' export const MOBILEDOC_VERSION = '0.3.1' -export const MOBILEDOC_MARKUP_SECTION_TYPE = 1 -export const MOBILEDOC_IMAGE_SECTION_TYPE = 2 -export const MOBILEDOC_LIST_SECTION_TYPE = 3 -export const MOBILEDOC_CARD_SECTION_TYPE = 10 - -export const MOBILEDOC_MARKUP_MARKER_TYPE = 0 -export const MOBILEDOC_ATOM_MARKER_TYPE = 1 const visitor = { [Type.POST](node: Post, opcodes: Opcodes) { @@ -57,42 +52,37 @@ const visitor = { }, } -type OpcodeCompilerMarker = [number, number[], number, unknown] -type OpcodeCompilerSection = - | [typeof MOBILEDOC_MARKUP_SECTION_TYPE, string, OpcodeCompilerMarker[]] - | [typeof MOBILEDOC_LIST_SECTION_TYPE, string, OpcodeCompilerMarker[][]] - | [typeof MOBILEDOC_IMAGE_SECTION_TYPE, string] - | [typeof MOBILEDOC_CARD_SECTION_TYPE, number] - -type OpcodeCompilerAtom = [string, unknown, {}] -type OpcodeCompilerCard = [string, {}] -type OpcodeCompilerMarkerType = [string, string[]?] - class PostOpcodeCompiler { markupMarkerIds!: number[] - markers!: OpcodeCompilerMarker[] - sections!: OpcodeCompilerSection[] - items!: OpcodeCompilerMarker[][] - markerTypes!: OpcodeCompilerMarkerType[] - atomTypes!: OpcodeCompilerAtom[] - cardTypes!: OpcodeCompilerCard[] + markers!: MobiledocMarker[] + sections!: MobiledocSection[] + items!: MobiledocMarker[][] + markerTypes!: MobiledocMarkerType[] + atomTypes!: MobiledocAtom[] + cardTypes!: MobiledocCard[] result!: MobiledocV0_3_1 _markerTypeCache!: Dict<number> - openMarker(closeCount: number, value: unknown) { + openMarker(closeCount: number, value: string) { this.markupMarkerIds = [] - this.markers.push([MOBILEDOC_MARKUP_MARKER_TYPE, this.markupMarkerIds, closeCount, value || '']) + this.markers.push([MobiledocMarkerKind.MARKUP, this.markupMarkerIds, closeCount, value || '']) + } + + openAtom(closeCount: number, name: string, value: string, payload: {}) { + const index = this._addAtomTypeIndex(name, value, payload) + this.markupMarkerIds = [] + this.markers.push([MobiledocMarkerKind.ATOM, this.markupMarkerIds, closeCount, index]) } openMarkupSection(tagName: string) { this.markers = [] - this.sections.push([MOBILEDOC_MARKUP_SECTION_TYPE, tagName, this.markers]) + this.sections.push([MobiledocSectionKind.MARKUP, tagName, this.markers]) } openListSection(tagName: string) { this.items = [] - this.sections.push([MOBILEDOC_LIST_SECTION_TYPE, tagName, this.items]) + this.sections.push([MobiledocSectionKind.LIST, tagName, this.items]) } openListItem() { @@ -101,18 +91,12 @@ class PostOpcodeCompiler { } openImageSection(url: string) { - this.sections.push([MOBILEDOC_IMAGE_SECTION_TYPE, url]) + this.sections.push([MobiledocSectionKind.IMAGE, url]) } openCardSection(name: string, payload: {}) { const index = this._addCardTypeIndex(name, payload) - this.sections.push([MOBILEDOC_CARD_SECTION_TYPE, index]) - } - - openAtom(closeCount: number, name: string, value: unknown, payload: {}) { - const index = this._addAtomTypeIndex(name, value, payload) - this.markupMarkerIds = [] - this.markers.push([MOBILEDOC_ATOM_MARKER_TYPE, this.markupMarkerIds, closeCount, index]) + this.sections.push([MobiledocSectionKind.CARD, index]) } openPost() { @@ -135,13 +119,13 @@ class PostOpcodeCompiler { } _addCardTypeIndex(cardName: string, payload: {}) { - let cardType: OpcodeCompilerCard = [cardName, payload] + let cardType: MobiledocCard = [cardName, payload] this.cardTypes.push(cardType) return this.cardTypes.length - 1 } - _addAtomTypeIndex(atomName: string, atomValue: unknown, payload: {}) { - let atomType: OpcodeCompilerAtom = [atomName, atomValue, payload] + _addAtomTypeIndex(atomName: string, atomValue: string, payload: {}) { + let atomType: MobiledocAtom = [atomName, atomValue, payload] this.atomTypes.push(atomType) return this.atomTypes.length - 1 } @@ -154,7 +138,7 @@ class PostOpcodeCompiler { let index = this._markerTypeCache[key] if (index === undefined) { - let markerType: OpcodeCompilerMarkerType = [tagName] + let markerType: MobiledocMarkerType = [tagName] if (attributesArray.length) { markerType.push(attributesArray) } @@ -170,10 +154,10 @@ class PostOpcodeCompiler { export interface MobiledocV0_3_1 { version: typeof MOBILEDOC_VERSION - atoms: OpcodeCompilerAtom[] - cards: OpcodeCompilerCard[] - markups: OpcodeCompilerMarkerType[] - sections: OpcodeCompilerSection[] + atoms: MobiledocAtom[] + cards: MobiledocCard[] + markups: MobiledocMarkerType[] + sections: MobiledocSection[] } /** diff --git a/src/js/renderers/mobiledoc/0-3-2.ts b/src/js/renderers/mobiledoc/0-3-2.ts index 882736b05..94cd9425f 100644 --- a/src/js/renderers/mobiledoc/0-3-2.ts +++ b/src/js/renderers/mobiledoc/0-3-2.ts @@ -11,15 +11,18 @@ import Marker from '../../models/marker' import Markup from '../../models/markup' import Atom from '../../models/atom' import { Dict } from '../../utils/types' +import { MobiledocSectionKind, MobiledocMarkerKind } from './constants' +import { MobiledocCard, MobiledocAtom, MobiledocMarker, MobiledocSection, MobiledocMarkerType } from './0-3' export const MOBILEDOC_VERSION = '0.3.2' -export const MOBILEDOC_MARKUP_SECTION_TYPE = 1 -export const MOBILEDOC_IMAGE_SECTION_TYPE = 2 -export const MOBILEDOC_LIST_SECTION_TYPE = 3 -export const MOBILEDOC_CARD_SECTION_TYPE = 10 -export const MOBILEDOC_MARKUP_MARKER_TYPE = 0 -export const MOBILEDOC_ATOM_MARKER_TYPE = 1 +export type MobiledocAttributedMarkupSection = [MobiledocSectionKind.MARKUP, string, MobiledocMarker[], string[]] +export type MobiledocAttributedListSection = [MobiledocSectionKind.LIST, string, MobiledocMarker[][], string[]] + +export type MobiledocAttributedSection = + | MobiledocSection + | MobiledocAttributedMarkupSection + | MobiledocAttributedListSection const visitor = { [Type.POST](node: Post, opcodes: Opcodes) { @@ -56,50 +59,44 @@ const visitor = { visitArray(visitor, node.openedMarkups, opcodes) }, } - -type OpcodeCompilerMarker = [number, number[], number, unknown] -type OpcodeCompilerSection = - | [typeof MOBILEDOC_MARKUP_SECTION_TYPE, string, OpcodeCompilerMarker[], string[]?] - | [typeof MOBILEDOC_LIST_SECTION_TYPE, string, OpcodeCompilerMarker[][], string[]?] - | [typeof MOBILEDOC_IMAGE_SECTION_TYPE, string] - | [typeof MOBILEDOC_CARD_SECTION_TYPE, number] - -type OpcodeCompilerAtom = [string, unknown, {}] -type OpcodeCompilerCard = [string, {}] -type OpcodeCompilerMarkerType = [string, string[]?] - class PostOpcodeCompiler { markupMarkerIds!: number[] - markers!: OpcodeCompilerMarker[] - sections!: OpcodeCompilerSection[] - items!: OpcodeCompilerMarker[][] - markerTypes!: OpcodeCompilerMarkerType[] - atomTypes!: OpcodeCompilerAtom[] - cardTypes!: OpcodeCompilerCard[] + markers!: MobiledocMarker[] + sections!: MobiledocAttributedSection[] + items!: MobiledocMarker[][] + markerTypes!: MobiledocMarkerType[] + atomTypes!: MobiledocAtom[] + cardTypes!: MobiledocCard[] result!: MobiledocV0_3_2 _markerTypeCache!: Dict<number> - openMarker(closeCount: number, value: unknown) { + openMarker(closeCount: number, value: string) { + this.markupMarkerIds = [] + this.markers.push([MobiledocMarkerKind.MARKUP, this.markupMarkerIds, closeCount, value || '']) + } + + openAtom(closeCount: number, name: string, value: string, payload: {}) { + const index = this._addAtomTypeIndex(name, value, payload) this.markupMarkerIds = [] - this.markers.push([MOBILEDOC_MARKUP_MARKER_TYPE, this.markupMarkerIds, closeCount, value || '']) + this.markers.push([MobiledocMarkerKind.ATOM, this.markupMarkerIds, closeCount, index]) } openMarkupSection(tagName: string, attributes: string[]) { this.markers = [] if (attributes && attributes.length !== 0) { - this.sections.push([MOBILEDOC_MARKUP_SECTION_TYPE, tagName, this.markers, attributes]) + this.sections.push([MobiledocSectionKind.MARKUP, tagName, this.markers, attributes]) } else { - this.sections.push([MOBILEDOC_MARKUP_SECTION_TYPE, tagName, this.markers]) + this.sections.push([MobiledocSectionKind.MARKUP, tagName, this.markers]) } } openListSection(tagName: string, attributes: string[]) { this.items = [] if (attributes && attributes.length !== 0) { - this.sections.push([MOBILEDOC_LIST_SECTION_TYPE, tagName, this.items, attributes]) + this.sections.push([MobiledocSectionKind.LIST, tagName, this.items, attributes]) } else { - this.sections.push([MOBILEDOC_LIST_SECTION_TYPE, tagName, this.items]) + this.sections.push([MobiledocSectionKind.LIST, tagName, this.items]) } } @@ -109,18 +106,12 @@ class PostOpcodeCompiler { } openImageSection(url: string) { - this.sections.push([MOBILEDOC_IMAGE_SECTION_TYPE, url]) + this.sections.push([MobiledocSectionKind.IMAGE, url]) } openCardSection(name: string, payload: {}) { const index = this._addCardTypeIndex(name, payload) - this.sections.push([MOBILEDOC_CARD_SECTION_TYPE, index]) - } - - openAtom(closeCount: number, name: string, value: unknown, payload: {}) { - const index = this._addAtomTypeIndex(name, value, payload) - this.markupMarkerIds = [] - this.markers.push([MOBILEDOC_ATOM_MARKER_TYPE, this.markupMarkerIds, closeCount, index]) + this.sections.push([MobiledocSectionKind.CARD, index]) } openPost() { @@ -143,13 +134,13 @@ class PostOpcodeCompiler { } _addCardTypeIndex(cardName: string, payload: {}) { - let cardType: OpcodeCompilerCard = [cardName, payload] + let cardType: MobiledocCard = [cardName, payload] this.cardTypes.push(cardType) return this.cardTypes.length - 1 } - _addAtomTypeIndex(atomName: string, atomValue: unknown, payload: {}) { - let atomType: OpcodeCompilerAtom = [atomName, atomValue, payload] + _addAtomTypeIndex(atomName: string, atomValue: string, payload: {}) { + let atomType: MobiledocAtom = [atomName, atomValue, payload] this.atomTypes.push(atomType) return this.atomTypes.length - 1 } @@ -162,7 +153,7 @@ class PostOpcodeCompiler { let index = this._markerTypeCache[key] if (index === undefined) { - let markerType: OpcodeCompilerMarkerType = [tagName] + let markerType: MobiledocMarkerType = [tagName] if (attributesArray.length) { markerType.push(attributesArray) } @@ -178,10 +169,10 @@ class PostOpcodeCompiler { export interface MobiledocV0_3_2 { version: typeof MOBILEDOC_VERSION - atoms: OpcodeCompilerAtom[] - cards: OpcodeCompilerCard[] - markups: OpcodeCompilerMarkerType[] - sections: OpcodeCompilerSection[] + atoms: MobiledocAtom[] + cards: MobiledocCard[] + markups: MobiledocMarkerType[] + sections: MobiledocAttributedSection[] } /** diff --git a/src/js/renderers/mobiledoc/0-3.ts b/src/js/renderers/mobiledoc/0-3.ts index 20bb4fef1..8b38e8747 100644 --- a/src/js/renderers/mobiledoc/0-3.ts +++ b/src/js/renderers/mobiledoc/0-3.ts @@ -11,15 +11,9 @@ import Marker from '../../models/marker' import Markup from '../../models/markup' import Atom from '../../models/atom' import { Dict } from '../../utils/types' +import { MobiledocSectionKind, MobiledocMarkerKind } from './constants' export const MOBILEDOC_VERSION = '0.3.0' -export const MOBILEDOC_MARKUP_SECTION_TYPE = 1 -export const MOBILEDOC_IMAGE_SECTION_TYPE = 2 -export const MOBILEDOC_LIST_SECTION_TYPE = 3 -export const MOBILEDOC_CARD_SECTION_TYPE = 10 - -export const MOBILEDOC_MARKUP_MARKER_TYPE = 0 -export const MOBILEDOC_ATOM_MARKER_TYPE = 1 const visitor = { [Type.POST](node: Post, opcodes: Opcodes) { @@ -57,42 +51,57 @@ const visitor = { }, } -type OpcodeCompilerMarker = [number, number[], number, unknown] -type OpcodeCompilerSection = - | [typeof MOBILEDOC_MARKUP_SECTION_TYPE, string, OpcodeCompilerMarker[]] - | [typeof MOBILEDOC_LIST_SECTION_TYPE, string, OpcodeCompilerMarker[][]] - | [typeof MOBILEDOC_IMAGE_SECTION_TYPE, string] - | [typeof MOBILEDOC_CARD_SECTION_TYPE, number] +export type MobiledocMarkupMarker = [MobiledocMarkerKind.MARKUP, number[], number, string] +export type MobiledocAtomMarker = [MobiledocMarkerKind.ATOM, number[], number, number] + +export type MobiledocMarker = MobiledocMarkupMarker | MobiledocAtomMarker + +export type MobiledocMarkupSection = [MobiledocSectionKind.MARKUP, string, MobiledocMarker[]] +export type MobiledocListSection = [MobiledocSectionKind.LIST, string, MobiledocMarker[][]] +export type MobiledocImageSection = [MobiledocSectionKind.IMAGE, string] +export type MobiledocCardSection = [MobiledocSectionKind.CARD, number] -type OpcodeCompilerAtom = [string, unknown, {}] -type OpcodeCompilerCard = [string, {}] -type OpcodeCompilerMarkerType = [string, string[]?] +export type MobiledocSection = + | MobiledocMarkupSection + | MobiledocListSection + | MobiledocImageSection + | MobiledocCardSection + +export type MobiledocAtom = [string, string, {}] +export type MobiledocCard = [string, {}] +export type MobiledocMarkerType = [string, string[]?] class PostOpcodeCompiler { markupMarkerIds!: number[] - markers!: OpcodeCompilerMarker[] - sections!: OpcodeCompilerSection[] - items!: OpcodeCompilerMarker[][] - markerTypes!: OpcodeCompilerMarkerType[] - atomTypes!: OpcodeCompilerAtom[] - cardTypes!: OpcodeCompilerCard[] + markers!: MobiledocMarker[] + sections!: MobiledocSection[] + items!: MobiledocMarker[][] + markerTypes!: MobiledocMarkerType[] + atomTypes!: MobiledocAtom[] + cardTypes!: MobiledocCard[] result!: MobiledocV0_3 _markerTypeCache!: Dict<number> - openMarker(closeCount: number, value: unknown) { + openMarker(closeCount: number, value: string) { + this.markupMarkerIds = [] + this.markers.push([MobiledocMarkerKind.MARKUP, this.markupMarkerIds, closeCount, value || '']) + } + + openAtom(closeCount: number, name: string, value: string, payload: {}) { + const index = this._addAtomTypeIndex(name, value, payload) this.markupMarkerIds = [] - this.markers.push([MOBILEDOC_MARKUP_MARKER_TYPE, this.markupMarkerIds, closeCount, value || '']) + this.markers.push([MobiledocMarkerKind.ATOM, this.markupMarkerIds, closeCount, index]) } openMarkupSection(tagName: string) { this.markers = [] - this.sections.push([MOBILEDOC_MARKUP_SECTION_TYPE, tagName, this.markers]) + this.sections.push([MobiledocSectionKind.MARKUP, tagName, this.markers]) } openListSection(tagName: string) { this.items = [] - this.sections.push([MOBILEDOC_LIST_SECTION_TYPE, tagName, this.items]) + this.sections.push([MobiledocSectionKind.LIST, tagName, this.items]) } openListItem() { @@ -101,18 +110,12 @@ class PostOpcodeCompiler { } openImageSection(url: string) { - this.sections.push([MOBILEDOC_IMAGE_SECTION_TYPE, url]) + this.sections.push([MobiledocSectionKind.IMAGE, url]) } openCardSection(name: string, payload: {}) { const index = this._addCardTypeIndex(name, payload) - this.sections.push([MOBILEDOC_CARD_SECTION_TYPE, index]) - } - - openAtom(closeCount: number, name: string, value: unknown, payload: {}) { - const index = this._addAtomTypeIndex(name, value, payload) - this.markupMarkerIds = [] - this.markers.push([MOBILEDOC_ATOM_MARKER_TYPE, this.markupMarkerIds, closeCount, index]) + this.sections.push([MobiledocSectionKind.CARD, index]) } openPost() { @@ -135,13 +138,13 @@ class PostOpcodeCompiler { } _addCardTypeIndex(cardName: string, payload: {}) { - let cardType: OpcodeCompilerCard = [cardName, payload] + let cardType: MobiledocCard = [cardName, payload] this.cardTypes.push(cardType) return this.cardTypes.length - 1 } - _addAtomTypeIndex(atomName: string, atomValue: unknown, payload: {}) { - let atomType: OpcodeCompilerAtom = [atomName, atomValue, payload] + _addAtomTypeIndex(atomName: string, atomValue: string, payload: {}) { + let atomType: MobiledocAtom = [atomName, atomValue, payload] this.atomTypes.push(atomType) return this.atomTypes.length - 1 } @@ -154,7 +157,7 @@ class PostOpcodeCompiler { let index = this._markerTypeCache[key] if (index === undefined) { - let markerType: OpcodeCompilerMarkerType = [tagName] + let markerType: MobiledocMarkerType = [tagName] if (attributesArray.length) { markerType.push(attributesArray) } @@ -170,10 +173,10 @@ class PostOpcodeCompiler { export interface MobiledocV0_3 { version: typeof MOBILEDOC_VERSION - atoms: OpcodeCompilerAtom[] - cards: OpcodeCompilerCard[] - markups: OpcodeCompilerMarkerType[] - sections: OpcodeCompilerSection[] + atoms: MobiledocAtom[] + cards: MobiledocCard[] + markups: MobiledocMarkerType[] + sections: MobiledocSection[] } /** diff --git a/src/js/renderers/mobiledoc/constants.ts b/src/js/renderers/mobiledoc/constants.ts new file mode 100644 index 000000000..5868b1a7e --- /dev/null +++ b/src/js/renderers/mobiledoc/constants.ts @@ -0,0 +1,11 @@ +export const enum MobiledocSectionKind { + MARKUP = 1, + IMAGE = 2, + LIST = 3, + CARD = 10, +} + +export const enum MobiledocMarkerKind { + MARKUP = 0, + ATOM = 1, +} diff --git a/src/js/utils/assert.ts b/src/js/utils/assert.ts index 96e0b6683..7bd0ef66c 100644 --- a/src/js/utils/assert.ts +++ b/src/js/utils/assert.ts @@ -1,6 +1,6 @@ import MobiledocError from './mobiledoc-error' -export default function (message: string, conditional: unknown): asserts conditional { +export default function assert(message: string, conditional: unknown): asserts conditional { if (!conditional) { throw new MobiledocError(message) } @@ -12,6 +12,10 @@ export function assertNotNull<T>(message: string, value: T | null): asserts valu } } +export function assertType<T>(message: string, _value: any, conditional: boolean): asserts _value is T { + assert(message, conditional) +} + export function expect<T>(value: T | null | undefined, message: string): T { if (value === null || value === undefined) { throw new MobiledocError(message) diff --git a/src/js/utils/cursor.ts b/src/js/utils/cursor.ts index 08220178e..8faa4939a 100644 --- a/src/js/utils/cursor.ts +++ b/src/js/utils/cursor.ts @@ -10,6 +10,7 @@ import Post from '../models/post' import { unwrap, assertNotNull, expect } from './assert' import { isCardSection } from '../models/card' import Section from '../models/_section' +import { Type } from '../models/types' export { Position, Range } @@ -95,7 +96,7 @@ class Cursor { } else { node = section.renderNode.element!.lastChild } - } else if (section.isBlank) { + } else if (section.isBlank || section.type === Type.IMAGE_SECTION) { node = section.renderNode.cursorElement offset = 0 } else { diff --git a/src/js/utils/cursor/position.ts b/src/js/utils/cursor/position.ts index 2c6c6f0ad..fbf4360a2 100644 --- a/src/js/utils/cursor/position.ts +++ b/src/js/utils/cursor/position.ts @@ -3,7 +3,7 @@ import RenderTree from '../../models/render-tree' import { isTextNode, containsNode, isElementNode } from '../dom-utils' import { findOffsetInNode } from '../selection-utils' import { DIRECTION } from '../key' -import assert from '../assert' +import assert, { assertType } from '../assert' import Range from './range' import Markerable from '../../models/_markerable' import Section from '../../models/_section' @@ -72,10 +72,6 @@ function assertIsCard(section: any): asserts section is Card { assert('findOffsetInSection must be called with markerable or card section', section && section.isCardSection) } -function assertType<T>(message: string, _value: any, conditional: boolean): asserts _value is T { - assert(message, conditional) -} - function isMarkerable(section: Section): section is Markerable { return section.isMarkerable } @@ -159,7 +155,7 @@ export default class Position { * (i.e., the marker to the left of the cursor if the cursor is on a marker boundary and text is left-to-right) * @return {Marker|undefined} */ - get marker(): Marker | null { + get marker(): Markuperable | null { return (this.isMarkerable && this.markerPosition.marker) || null } diff --git a/src/js/utils/cursor/range.ts b/src/js/utils/cursor/range.ts index 7b9e61673..ec5047365 100644 --- a/src/js/utils/cursor/range.ts +++ b/src/js/utils/cursor/range.ts @@ -2,9 +2,9 @@ import Position from './position' import { DIRECTION } from '../key' import assert, { assertNotNull, unwrap } from '../assert' import Markerable from '../../models/_markerable' -import Marker from '../../models/marker' import MobiledocError from '../mobiledoc-error' import Section from '../../models/_section' +import Markuperable from '../markuperable' export type Direction = DIRECTION | null /** @@ -139,7 +139,7 @@ export default class Range { * * @public */ - expandByMarker(detectMarker: (marker: Marker) => boolean) { + expandByMarker(detectMarker: (marker: Markuperable) => boolean) { let { head, tail, direction } = this let { section: headSection } = head @@ -152,11 +152,15 @@ export default class Range { ) } - let firstNotMatchingDetect = (i: Marker) => { + let firstNotMatchingDetect = (i: Markuperable) => { return !detectMarker(i) } - let headMarker: Marker | null | undefined = headSection.markers.detect(firstNotMatchingDetect, head.marker, true) + let headMarker: Markuperable | null | undefined = headSection.markers.detect( + firstNotMatchingDetect, + head.marker, + true + ) if (!headMarker && detectMarker(headSection.markers.head!)) { headMarker = headSection.markers.head } else { diff --git a/src/js/utils/dom-utils.ts b/src/js/utils/dom-utils.ts index 347fae0a2..e7877fb5f 100644 --- a/src/js/utils/dom-utils.ts +++ b/src/js/utils/dom-utils.ts @@ -10,7 +10,7 @@ export function isTextNode(node: Node): node is Text { return node.nodeType === NODE_TYPES.TEXT } -export function isCommentNode(node: Node) { +export function isCommentNode(node: Node): node is Comment { return node.nodeType === NODE_TYPES.COMMENT } diff --git a/src/js/utils/element-utils.ts b/src/js/utils/element-utils.ts index 45d2be18f..e999e4b8d 100644 --- a/src/js/utils/element-utils.ts +++ b/src/js/utils/element-utils.ts @@ -69,7 +69,11 @@ export function setData(element: HTMLElement, name: string, value: string) { } } -export function whenElementIsNotInDOM(element: HTMLElement, callback: () => void) { +export interface Cancelable { + cancel(): void +} + +export function whenElementIsNotInDOM(element: HTMLElement, callback: () => void): Cancelable { let isCanceled = false const observerFn = () => { if (isCanceled) { diff --git a/src/js/utils/log-manager.ts b/src/js/utils/log-manager.ts index fec68a029..9a99707af 100644 --- a/src/js/utils/log-manager.ts +++ b/src/js/utils/log-manager.ts @@ -1,4 +1,4 @@ -class Logger { +export class Logger { type: string manager: LogManager diff --git a/src/js/utils/markuperable.ts b/src/js/utils/markuperable.ts index c88200ffb..a9935d2b3 100644 --- a/src/js/utils/markuperable.ts +++ b/src/js/utils/markuperable.ts @@ -1,8 +1,10 @@ import { normalizeTagName } from './dom-utils' import { detect, commonItemLength, forEach, filter } from './array-utils' +import { Option } from './types' import Markup from '../models/markup' import RenderNode from '../models/render-node' import { Type } from '../models/types' +import Markerable from '../models/_markerable' type MarkupCallback = (markup: Markup) => boolean type MarkupOrMarkupCallback = Markup | MarkupCallback @@ -16,10 +18,24 @@ export default abstract class Markuperable { isAtom = false isMarker = false + section: Option<Markerable> = null + parent: Option<Markerable> = null + renderNode: RenderNode | null = null + abstract text: string + abstract value: string abstract type: Type abstract length: number + abstract clone(): Markuperable + abstract isBlank: boolean + abstract canJoin(other: Markuperable): boolean + abstract textUntil(offset: number): string + abstract splitAtOffset(offset: number): [Markuperable, Markuperable] + + charAt(offset: number) { + return this.value.slice(offset, offset + 1) + } clearMarkups() { this.markups = [] diff --git a/src/js/utils/object-utils.ts b/src/js/utils/object-utils.ts index 7636e54c0..7acd93ecc 100644 --- a/src/js/utils/object-utils.ts +++ b/src/js/utils/object-utils.ts @@ -1,7 +1,7 @@ -export function entries<T extends { [key: string]: unknown }, K extends keyof T>(obj: T): [keyof T, T[K]][] { +export function entries<T extends { [key: string]: unknown }, K extends Extract<keyof T, string>>(obj: T): [K, T[K]][] { const ownProps = Object.keys(obj) as K[] let i = ownProps.length - const resArray = new Array<[keyof T, T[K]]>(i) + const resArray = new Array<[K, T[K]]>(i) while (i--) { resArray[i] = [ownProps[i], obj[ownProps[i]]] diff --git a/src/js/utils/parse-utils.js b/src/js/utils/parse-utils.ts similarity index 71% rename from src/js/utils/parse-utils.js rename to src/js/utils/parse-utils.ts index dde21b1df..ba1456356 100644 --- a/src/js/utils/parse-utils.js +++ b/src/js/utils/parse-utils.ts @@ -1,7 +1,12 @@ -/* global JSON */ import mobiledocParsers from '../parsers/mobiledoc' import HTMLParser from '../parsers/html' import TextParser from '../parsers/text' +import PostNodeBuilder from '../models/post-node-builder' +import { SectionParserPlugin } from '../parsers/section' +import Post from '../models/post' +import { Logger } from './log-manager' +import Editor from '../editor/editor' +import { Maybe } from './types' export const MIME_TEXT_PLAIN = 'text/plain' export const MIME_TEXT_HTML = 'text/html' @@ -13,11 +18,11 @@ const MOBILEDOC_REGEX = new RegExp(/data-mobiledoc='(.*?)'>/) * @return {Post} * @private */ -function parsePostFromHTML(html, builder, plugins) { - let post +function parsePostFromHTML(html: string, builder: PostNodeBuilder, plugins: SectionParserPlugin[]): Post { + let post: Post if (MOBILEDOC_REGEX.test(html)) { - let mobiledocString = html.match(MOBILEDOC_REGEX)[1] + let mobiledocString = html.match(MOBILEDOC_REGEX)![1] let mobiledoc = JSON.parse(mobiledocString) post = mobiledocParsers.parse(builder, mobiledoc) } else { @@ -31,17 +36,22 @@ function parsePostFromHTML(html, builder, plugins) { * @return {Post} * @private */ -function parsePostFromText(text, builder, plugins) { +function parsePostFromText(text: string, builder: PostNodeBuilder, plugins: SectionParserPlugin[]): Post { let parser = new TextParser(builder, { plugins }) let post = parser.parse(text) return post } +// Extend TypeScript's Window interface to include clipboardData from events +interface Window { + readonly clipboardData: DataTransfer | null +} + /** * @return {{html: String, text: String}} * @private */ -export function getContentFromPasteEvent(event, window) { +export function getContentFromPasteEvent(event: ClipboardEvent, window: Window) { let html = '', text = '' @@ -65,13 +75,13 @@ export function getContentFromPasteEvent(event, window) { * @return {{html: String, text: String}} * @private */ -function getContentFromDropEvent(event, logger) { +function getContentFromDropEvent(event: DragEvent, logger?: Logger): { html: string; text: string } { let html = '', text = '' try { - html = event.dataTransfer.getData(MIME_TEXT_HTML) - text = event.dataTransfer.getData(MIME_TEXT_PLAIN) + html = event.dataTransfer!.getData(MIME_TEXT_HTML) + text = event.dataTransfer!.getData(MIME_TEXT_PLAIN) } catch (e) { // FIXME IE11 does not include any data in the 'text/html' or 'text/plain' // mimetypes. It throws an error 'Invalid argument' when attempting to read @@ -90,7 +100,7 @@ function getContentFromDropEvent(event, logger) { * @param {Window} * @private */ -export function setClipboardData(event, { mobiledoc, html, text }, window) { +export function setClipboardData(event: ClipboardEvent, { mobiledoc, html, text }: Editor, window: Window) { if (mobiledoc && html) { html = `<div data-mobiledoc='${JSON.stringify(mobiledoc)}'>${html}</div>` } @@ -116,11 +126,11 @@ export function setClipboardData(event, { mobiledoc, html, text }, window) { * @private */ export function parsePostFromPaste( - pasteEvent, - { builder, _parserPlugins: plugins }, + pasteEvent: ClipboardEvent, + { builder, _parserPlugins: plugins }: Editor, { targetFormat } = { targetFormat: 'html' } -) { - let { html, text } = getContentFromPasteEvent(pasteEvent, window) +): Maybe<Post> { + let { html, text } = getContentFromPasteEvent(pasteEvent, (window as unknown) as ClipboardEvent) if (targetFormat === 'html' && html && html.length) { return parsePostFromHTML(html, builder, plugins) @@ -136,7 +146,11 @@ export function parsePostFromPaste( * @return {Post} * @private */ -export function parsePostFromDrop(dropEvent, editor, { logger } = {}) { +export function parsePostFromDrop( + dropEvent: DragEvent, + editor: Editor, + { logger }: { logger?: Logger } = {} +): Maybe<Post> { let { builder, _parserPlugins: plugins } = editor let { html, text } = getContentFromDropEvent(dropEvent, logger) diff --git a/src/js/views/tooltip.js b/src/js/views/tooltip.ts similarity index 71% rename from src/js/views/tooltip.js rename to src/js/views/tooltip.ts index a1d77c509..4e311009b 100644 --- a/src/js/views/tooltip.js +++ b/src/js/views/tooltip.ts @@ -1,14 +1,34 @@ import View from './view' -import { positionElementCenteredBelow, getEventTargetMatchingTag, whenElementIsNotInDOM } from '../utils/element-utils' +import { + positionElementCenteredBelow, + getEventTargetMatchingTag, + whenElementIsNotInDOM, + Cancelable, +} from '../utils/element-utils' import { editLink } from '../editor/ui' const SHOW_DELAY = 200 const HIDE_DELAY = 600 +type Editor = any + +interface TooltipOptions { + rootElement: HTMLElement + editor: Editor + showForTag: string +} + +interface AddListenerOptions { + showForTag: string +} + export default class Tooltip extends View { - constructor(options) { - options.classNames = ['__mobiledoc-tooltip'] - super(options) + rootElement: HTMLElement + editor: any + elementObserver: Cancelable | null = null + + constructor(options: TooltipOptions) { + super({ ...options, classNames: ['__mobiledoc-tooltip'] }) this.rootElement = options.rootElement this.editor = options.editor @@ -16,7 +36,7 @@ export default class Tooltip extends View { this.addListeners(options) } - showLink(linkEl) { + showLink(linkEl: HTMLElement) { const { editor, element: tooltipEl } = this const { tooltipPlugin } = editor @@ -33,9 +53,9 @@ export default class Tooltip extends View { this.elementObserver = whenElementIsNotInDOM(linkEl, () => this.hide()) } - addListeners(options) { + addListeners(options: AddListenerOptions) { const { rootElement, element: tooltipElement } = this - let showTimeout, hideTimeout + let showTimeout: number, hideTimeout: number const scheduleHide = () => { clearTimeout(hideTimeout) @@ -53,12 +73,12 @@ export default class Tooltip extends View { }) this.addEventListener(rootElement, 'mouseover', event => { - let target = getEventTargetMatchingTag(options.showForTag, event.target, rootElement) + let target = getEventTargetMatchingTag(options.showForTag, event.target as HTMLElement, rootElement) if (target && target.isContentEditable) { clearTimeout(hideTimeout) showTimeout = setTimeout(() => { - this.showLink(target) + target && this.showLink(target) }, SHOW_DELAY) } }) @@ -74,7 +94,7 @@ export default class Tooltip extends View { } export const DEFAULT_TOOLTIP_PLUGIN = { - renderLink(tooltipEl, linkEl, { editLink }) { + renderLink(tooltipEl: Element, linkEl: HTMLLinkElement, { editLink }) { const { href } = linkEl tooltipEl.innerHTML = `<a href="${href}" target="_blank">${href}</a>` const button = document.createElement('button') diff --git a/src/js/views/view.js b/src/js/views/view.ts similarity index 70% rename from src/js/views/view.js rename to src/js/views/view.ts index 1e85a613d..534a6356f 100644 --- a/src/js/views/view.js +++ b/src/js/views/view.ts @@ -1,20 +1,35 @@ import { addClassName } from '../utils/dom-utils' +interface ViewOptions { + tagName: string + container: HTMLElement + classNames: string[] +} + +type EventType = keyof HTMLElementEventMap + class View { - constructor(options = {}) { + element: HTMLElement + container: HTMLElement + + isShowing: boolean = false + isDestroyed: boolean = false + + _eventListeners: [HTMLElement, EventType, EventListener][] + + constructor(options: Partial<ViewOptions> = {}) { options.tagName = options.tagName || 'div' options.container = options.container || document.body this.element = document.createElement(options.tagName) this.container = options.container - this.isShowing = false let classNames = options.classNames || [] classNames.forEach(name => addClassName(this.element, name)) this._eventListeners = [] } - addEventListener(element, type, listener) { + addEventListener(element: HTMLElement, type: EventType, listener: EventListener) { element.addEventListener(type, listener) this._eventListeners.push([element, type, listener]) } diff --git a/tests/acceptance/editor-copy-paste-test.js b/tests/acceptance/editor-copy-paste-test.js index bfd0785c3..2562e00ca 100644 --- a/tests/acceptance/editor-copy-paste-test.js +++ b/tests/acceptance/editor-copy-paste-test.js @@ -132,7 +132,6 @@ test('willCopy callback called before copy', (assert) => { editor.addCallback('willCopy', data => { assert.deepEqual(data.mobiledoc, mobiledoc); data.mobiledoc.sections[0][1] = 'blockquote'; - console.log({ data }) }); editor.render(editorElement); diff --git a/tests/helpers/post-abstract.js b/tests/helpers/post-abstract.js index 7e0eca7d9..73c3a9932 100644 --- a/tests/helpers/post-abstract.js +++ b/tests/helpers/post-abstract.js @@ -29,6 +29,7 @@ function build(treeFn) { } let cardRegex = /\[(.*)\]/; +let imageSectionRegex = /^\{(.*)\}/; let markupRegex = /\*/g; let listStartRegex = /^\* /; let cursorRegex = /<|>|\|/g; @@ -177,6 +178,8 @@ function parseSingleText(text, builder) { if (cardRegex.test(text)) { section = builder.cardSection(cardRegex.exec(text)[1]); + } else if (imageSectionRegex.test(text)) { + section = builder.imageSection(imageSectionRegex.exec(text)[1]); } else { let type = 'p'; if (listStartRegex.test(text)) { @@ -224,6 +227,7 @@ function parseSingleText(text, builder) { * buildFromText(["abc","def"]) -> { post } with 2 markups sections ("p") with texts "abc" and "def" * buildFromText("abc|def") -> { post, range } where range is collapsed at offset 3 (after the "c") * buildFromText(["abcdef","[some-card]","def"]) -> { post } with [MarkupSection, Card, MarkupSection] sections + * buildFromText(["abc", "{def}", "def"]) -> { post } with [MarkupSection, ImageSection, MarkupSection] sections * buildFromText(["* item 1", "* item 2"]) -> { post } with a ListSection with 2 ListItems * buildFromText(["<abc", "def", "ghi>"]) -> { post, range } where range is the entire post (before the "a" to after the "i") */ diff --git a/tests/unit/editor/post/insert-post-test.js b/tests/unit/editor/post/insert-post-test.js index 8b474cddf..1bdcabc75 100644 --- a/tests/unit/editor/post/insert-post-test.js +++ b/tests/unit/editor/post/insert-post-test.js @@ -26,6 +26,9 @@ let blankSectionExpecations = [ ['*abc*'], // section with markup ['[my-card]'], // single card ['[my-card]', '[my-other-card]'], // multiple cards + // Image section test is failing only in Safari due to selection ranges not + // accepting img elements + // ['{my-image}'], // single image section ['abc','* 123','* 456','[my-card]'], // mixed ]; blankSectionExpecations.forEach(dsl => { diff --git a/tsconfig.json b/tsconfig.json index 0722840b2..21ad7a8ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,12 +42,12 @@ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ + "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */