Skip to content

Commit

Permalink
✨👑✨ Make Annotations classes instead of JS objects (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-evans committed Aug 17, 2018
1 parent 1006125 commit e3f5ed7
Show file tree
Hide file tree
Showing 18 changed files with 2,403 additions and 3,340 deletions.
4,740 changes: 1,698 additions & 3,042 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"private": true,
"name": "atjson",
"devDependencies": {
"@types/jest": "^22.2.3",
"@types/jest": "^23.0.2",
"husky": "^1.0.0-rc.13",
"jest": "^22.4.2",
"lerna": "^3.0.0-rc.0",
"ts-jest": "^22.4.4",
"tslint": "^5.9.1",
"jest": "^23.1.0",
"lerna": "^3.0.6",
"ts-jest": "^22.4.6",
"tslint": "^5.10.0",
"typedoc": "^0.11.1",
"typedoc-plugin-monorepo": "^0.1.0",
"typescript": "^2.8.1"
"typescript": "^2.8.3"
},
"scripts": {
"build": "lerna run build",
Expand Down
147 changes: 135 additions & 12 deletions packages/@atjson/document/src/annotation.ts
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)
};
}
}
52 changes: 52 additions & 0 deletions packages/@atjson/document/src/attributes.ts
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;
}
}
7 changes: 7 additions & 0 deletions packages/@atjson/document/src/block-annotation.ts
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;
}
}
31 changes: 31 additions & 0 deletions packages/@atjson/document/src/change.ts
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();
}
}
Loading

0 comments on commit e3f5ed7

Please sign in to comment.