-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Annotations classes instead of JS objects #54
Changes from all commits
74caf47
53297c2
62a73c8
e71ad2d
0b8cbed
ea592a3
15eb0b0
8c0d805
ae08b75
804be32
a669592
a6b84b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trivial - could this be combined as also maybe I'm reading it wrong but I think you could. |
||
|
||
// 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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. silencing tslint because it would have complained about an empty block? but do you need this block at all? or is it here just to serve as living documentation? (an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Living documentation here. |
||
} | ||
|
||
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) | ||
}; | ||
} | ||
} |
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; | ||
} | ||
} |
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; | ||
} | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yasss |
||
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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍