Skip to content
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

Merged
merged 12 commits into from
Jun 25, 2018
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"private": true,
"name": "atjson",
"devDependencies": {
"@types/jest": "^22.2.3",
"jest": "^22.4.2",
"lerna": "^2.8.0",
"ts-jest": "^22.4.4",
"tslint": "^5.9.1",
"@types/jest": "^23.0.2",
"jest": "^23.1.0",
"lerna": "^2.11.0",
"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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


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) {
Copy link
Contributor

@pgoldrbx pgoldrbx Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trivial - could this be combined as else if to reduce conditional nesting?
(just to make things a tad easier to follow)

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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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 else case would also do nothing)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)
};
}
}
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
}
}
Loading