-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨👑✨ Make Annotations classes instead of JS objects (#54)
- Loading branch information
Showing
18 changed files
with
2,403 additions
and
3,340 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,141 @@ | ||
import { Display } from './schema'; | ||
import { Attributes, toJSON, unprefix } from './attributes'; | ||
import Change, { AdjacentBoundaryBehaviour, Deletion, Insertion } from './change'; | ||
|
||
export default interface Annotation { | ||
id?: string | number; | ||
type: string; | ||
display?: Display; | ||
export default abstract class Annotation { | ||
static vendorPrefix: string; | ||
static type: string; | ||
readonly type: string; | ||
abstract rank: number; | ||
start: number; | ||
end: number; | ||
attributes: Attributes; | ||
|
||
attributes?: { [key: string]: any }; | ||
constructor(attrs: { start: number, end: number, attributes: Attributes }) { | ||
let AnnotationClass = this.constructor as typeof Annotation; | ||
this.type = AnnotationClass.type; | ||
this.start = attrs.start; | ||
this.end = attrs.end; | ||
this.attributes = unprefix(AnnotationClass.vendorPrefix, attrs.attributes) as Attributes; | ||
} | ||
|
||
transform?: ( | ||
annotation: Annotation, | ||
content: string, | ||
position: number, | ||
length: number, | ||
preserveAdjacentBoundaries: boolean) => void; | ||
/** | ||
* nb. Currently, changes are applied directly to the document. | ||
* In the future, we want to return a set of changes that | ||
* are applied to the document including annotations. | ||
*/ | ||
handleChange(change: Change) { | ||
if (change.type === 'insertion') { | ||
this.handleInsertion(change as Insertion); | ||
} else { | ||
this.handleDeletion(change as Deletion); | ||
} | ||
} | ||
|
||
handleDeletion(change: Deletion) { | ||
let length = change.end - change.start; | ||
|
||
// We're deleting after the annotation, nothing needed to be done. | ||
// [ ] | ||
// -----------*---*--- | ||
if (this.end < change.start) return; | ||
|
||
// If the annotation is wholly *after* the deleted text, just move | ||
// everything. | ||
// [ ] | ||
// --*---*------------- | ||
if (change.end < this.start) { | ||
this.start -= length; | ||
this.end -= length; | ||
|
||
} else { | ||
|
||
if (change.end < this.end) { | ||
|
||
// Annotation spans the whole deleted text, so just truncate the end of | ||
// the annotation (shrink from the right). | ||
// [ ] | ||
// ------*------*--------- | ||
if (change.start > this.start) { | ||
this.end -= length; | ||
|
||
// Annotation occurs within the deleted text, affecting both start and | ||
// end of the annotation, but by only part of the deleted text length. | ||
// [ ] | ||
// ---*---------*------------ | ||
} else if (change.start <= this.start) { | ||
this.start -= this.start - change.start; | ||
this.end -= length; | ||
} | ||
|
||
} else if (change.end >= this.end) { | ||
|
||
// [ ] | ||
// [ ] | ||
// [ ] | ||
// [ ] | ||
// ------*---------*-------- | ||
if (change.start <= this.start) { | ||
this.start = change.start; | ||
this.end = change.start; | ||
|
||
// [ ] | ||
// ------*---------*-------- | ||
} else if (change.start > this.start) { | ||
this.end = change.start; | ||
} | ||
} | ||
} | ||
} | ||
|
||
handleInsertion(change: Insertion) { | ||
let length = change.text.length; | ||
|
||
// The first two normal cases are self explanatory. Just adjust the annotation | ||
// position, since there is never a case where we wouldn't want to. | ||
if (change.start < this.start) { | ||
this.start += length; | ||
this.end += length; | ||
} else if (change.start > this.start && change.start < this.end) { | ||
this.end += length; | ||
|
||
// In this case, however, the normal behaviour when inserting text at a | ||
// point adjacent to an annotation is to drag along the end of the | ||
// annotation, or push forward the beginning, i.e., the transform happens | ||
// _inside_ an annotation to the left, or _outside_ an annotation to the right. | ||
// | ||
// Sometimes, the desire is to change the direction; this is provided below | ||
// with the preserveAdjacentBoundaries switch. | ||
|
||
// Default edge behaviour. | ||
} else if (change.behaviour === AdjacentBoundaryBehaviour.default) { | ||
if (change.start === this.start) { | ||
this.start += length; | ||
this.end += length; | ||
} else if (change.start === this.end) { | ||
this.end += length; | ||
} | ||
|
||
// Non-standard behaviour. Do nothing to the adjacent boundary! | ||
} else if (change.behaviour === AdjacentBoundaryBehaviour.preserve && change.start === this.start) { | ||
this.end += length; | ||
|
||
// no-op; we would delete the annotation, but we should defer to the | ||
// annotation as to whether or not it's deletable, since some zero-length | ||
// annotations should be retained. | ||
// n.b. the += 0 is just to silence tslint ;-) | ||
} else if (change.start === this.end) { | ||
this.end += 0; | ||
} | ||
} | ||
|
||
toJSON() { | ||
let AnnotationClass = this.constructor as typeof Annotation; | ||
let vendorPrefix = AnnotationClass.vendorPrefix; | ||
return { | ||
type: `-${vendorPrefix}-${this.type}`, | ||
start: this.start, | ||
end: this.end, | ||
attributes: toJSON(vendorPrefix, this.attributes) | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import Document from './index'; | ||
import JSON, { Dictionary, JSONObject } from './json'; | ||
|
||
export type Attribute = string | number | boolean | null | Attributes | AttributeArray; | ||
export interface Attributes extends Dictionary<Attribute> {} | ||
export interface AttributeArray extends Array<Attribute> {} | ||
|
||
export function unprefix(vendorPrefix: string, attribute: Attribute): Attribute { | ||
if (Array.isArray(attribute)) { | ||
return attribute.map(attr => { | ||
let result = unprefix(vendorPrefix, attr); | ||
return result; | ||
}); | ||
} else if (attribute instanceof Document) { | ||
return attribute; | ||
} else if (attribute == null) { | ||
return null; | ||
} else if (typeof attribute === 'object') { | ||
return Object.keys(attribute).reduce((attrs: Attributes, key: string) => { | ||
let value = attrs[key]; | ||
if (key.indexOf(`-${vendorPrefix}-`) === 0 && value !== undefined) { | ||
attrs[key.slice(`-${vendorPrefix}-`.length)] = unprefix(vendorPrefix, value); | ||
} | ||
return attrs; | ||
}, {}); | ||
} else { | ||
return attribute; | ||
} | ||
} | ||
|
||
export function toJSON(vendorPrefix: string, attribute: Attribute): JSON { | ||
if (Array.isArray(attribute)) { | ||
return attribute.map(attr => { | ||
let result = toJSON(vendorPrefix, attr); | ||
return result; | ||
}); | ||
} else if (attribute instanceof Document) { | ||
return attribute.toJSON(); | ||
} else if (attribute == null) { | ||
return null; | ||
} else if (typeof attribute === 'object') { | ||
return Object.keys(attribute).reduce((copy: JSONObject, key: string) => { | ||
let value = attribute[key]; | ||
if (value !== undefined) { | ||
copy[key] = toJSON(vendorPrefix, value); | ||
} | ||
return copy; | ||
}, {}); | ||
} else { | ||
return attribute; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Annotation from './annotation'; | ||
|
||
export default abstract class BlockAnnotation extends Annotation { | ||
get rank() { | ||
return 10; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
export default abstract class Change { | ||
abstract readonly type: string; | ||
} | ||
|
||
export enum AdjacentBoundaryBehaviour { | ||
preserve, | ||
default, | ||
modify | ||
} | ||
|
||
export class Deletion extends Change { | ||
readonly type = 'deletion'; | ||
constructor( | ||
readonly start: number, | ||
readonly end: number, | ||
readonly behaviour: AdjacentBoundaryBehaviour = AdjacentBoundaryBehaviour.default | ||
) { | ||
super(); | ||
} | ||
} | ||
|
||
export class Insertion extends Change { | ||
readonly type = 'insertion'; | ||
constructor( | ||
readonly start: number, | ||
readonly text: string, | ||
readonly behaviour: AdjacentBoundaryBehaviour = AdjacentBoundaryBehaviour.default | ||
) { | ||
super(); | ||
} | ||
} |
Oops, something went wrong.