1+ /**
2+ * Cobalt simplified HTML rules and rendering
3+ */
14cobalt . html = ( function ( html ) {
25
6+ /**
7+ * These rules define the behaviour of the rendering as well as the editor.
8+ */
39 var rules = {
410 block : [ 'h1' , 'h2' , 'h3' , 'p' , 'ol' , 'ul' , 'li' , 'blockquote' , 'br' ] ,
511 inline : [ 'em' , 'strong' , 'a' ] ,
@@ -9,7 +15,17 @@ cobalt.html = (function(html) {
915 } ,
1016 obligatoryParent : {
1117 'li' : [ 'ol' , 'ul' ]
12- }
18+ } ,
19+ nextTag : {
20+ 'h1' : 'p' ,
21+ 'h2' : 'p' ,
22+ 'h3' : 'p' ,
23+ 'p' : 'p' ,
24+ 'li' : 'li'
25+ } ,
26+ cannotHaveChildren : {
27+ 'br'
28+ } ,
1329 } ;
1430 rules . alltags = rules . block . concat ( rules . inline ) ;
1531 rules . nesting : {
@@ -27,25 +43,153 @@ cobalt.html = (function(html) {
2743 } ;
2844 rules . toplevel = rules . block . filter ( function ( tag ) { return tag != 'li' ; } ) ;
2945
46+ var constraints = {
47+ hasValidParent : function ( entry , stack , position ) {
48+ var parent = stack [ position ] ? stack [ position ] : { tag :'' } ;
49+ return rules . nesting [ parent . tag ] . includes ( entry . tag ) ;
50+ } ,
51+ hasValidChild : function ( entry , stack , position ) {
52+ var child = stack [ position + 1 ] ? stack [ position + 1 ] : { tag :'' } ;
53+ var tag = entry . tag ;
54+ if ( rules . obligatoryChild ( tag ) ) {
55+ tag = rules . obligatoryChild ( entry . tag ) ;
56+ }
57+ return rules . cannotHaveChildren ( tag ) // e.g. BR, is autoclosing, so other tags aren't contained by it
58+ || rules . nesting [ tag ] . includes ( child . tag ) ;
59+ } ,
60+ scorePosition : function ( entry , stack , position ) {
61+ // return a number - 0 is optimal, anything larger is less optimal
62+ // best position means that all parents have starting range offset <= this
63+ // all children have starting range offset >= this
64+ // add offsets that violate this to the score with the score the distance between this start and theirs.
65+ }
66+ }
67+
68+ function getRelativeList ( annotations ) {
69+ if ( ! annotations || ! annotations . length ) { return [ ] ; }
70+ var list = [ ] ;
71+ annotations . foreach ( function ( annotation ) {
72+ annotation . range . foreach ( function ( range )
73+ list . push ( {
74+ type : 'start' ,
75+ annotation : annotation ,
76+ position : range . start
77+ } ) ;
78+ list . push ( {
79+ type : 'end' ,
80+ annotation : annotation ,
81+ position : range . end
82+ } ) ;
83+ } ) ;
84+ } ) ;
85+ list . sort ( function ( a , b ) {
86+ return a . offset < b . offset ? - 1 : 1 ;
87+ } ) ;
88+ list . reduce ( function ( position , entry ) {
89+ entry . offset = entry . position - position ;
90+ delete entry . position ;
91+ return position + entry . offset ;
92+ } , 0 ) ;
93+ return list ;
94+ }
95+
96+ function arrangeTags ( tags ) {
97+ // get best scoring set of tags that also validate the rules
98+ // tags that start later have a bonus over tags that start earlier
99+ // so more specific tags override more generic tags
100+ // each tag has the same score for now, 1
101+ // so more valid tags is better
102+ // stack order indicates html order, parent in stack is a parent in html
103+ // so nesting rules must be valid through the stack
104+
105+ // 0: find nearest blocklevel element, add it to the stack
106+ // 1: in order, add as much block level tags as you can, start at the top of the stack, search untill
107+ // a place in the stack is valid for this tag, so a valid parent and a valid child
108+ // any tag that has no place is kept in a temporary list
109+ // tags that have a start offset before the current tag, prefer to be in the stack above the current tag
110+ // tags that start later, prefer to be lower in the stack
111+ // this makes html nesting behave as expected most of the time, unless this breaks other rules
112+ // so search for the optimal place in the stack first, then go up the stack, round robin over position 0,
113+ // untill you reach your start position or you find a place for the tag
114+ // 2: do the same for inline elements
115+ // 3: foreach skipped element create a stack with that element as the first element, add as many elements
116+ // as possible, inline elements need a valid block element as parent, so make sure you find one or skip
117+ // the element entirely if no valid parent exists.
118+ // 3: tallest stack wins
119+ // note: ol en ul add an li for each subrange automatically, so you don't need to specify li in
120+ // the cobalt fragment by hand, just cut the range in parts, seperated by at least 1 char, e.g. \n
121+ // e.g: "0-10,12-20:ol" should be enough
122+
123+ // approach:
124+ // create a list of functions that check constraints, returning true if the constraint is met, false if not
125+ // when considering a position in the stack for a tag/element, check each constraint.
126+ // update: allow numeric scores, 0 is optimal, negative numbers means invalid
127+ // can this be used for a mathematically sound constraint based approach?
128+ }
129+
130+ function getStackList ( relativeList ) {
131+ // 1: create a list of stacks of tags - this version ignores empty elements/tags
132+ var stacklist = [ { offset : 0 , tags : [ ] } ] ;
133+ relativeList . forEach ( function ( entry ) {
134+ if ( entry . offset ) {
135+ stacklist . push ( { offset : entry . offset , tags : stacklist [ stacklist . length - 1 ] . tags } ) ;
136+ }
137+ stackentry = stacklist [ stacklist . length - 1 ] ;
138+ if ( entry . type == 'start' ) {
139+ stackentry . tags . push ( entry . annotation ) ;
140+ } else if ( entry . type == 'end' ) {
141+ stackentry . tags = stackentry . tags . filter ( function ( annotation ) {
142+ if ( annotation == entry . annotation ) {
143+ return false ;
144+ }
145+ return true ;
146+ } ) ;
147+ }
148+ } ) ;
149+ // 2: rearrange / filter tags to match the rules
150+ // FIMXE: entry.tags has no information if an annotation is used more than once
151+ // this means we cannot assure an html id is only used once, so all id's must be renamed to
152+ // something else, e.g. data-cobalt-id=id, but this can also be done when merging
153+ stacklist . forEach ( function ( entry ) {
154+ entry . tags = arrangeTags ( entry . tags ) ;
155+ } ) ;
156+ return stacklist ;
157+ }
158+
159+ function mergeContentAndTags ( content , stacklist ) {
160+
161+ }
30162
31163 html . cobaltToHtml = function ( fragment ) {
32- var result = '' ;
33-
34- return result ;
164+ /*
165+ TODO: find a way to break up a single cobaltToHtml call into many seperate
166+ calls that can be concatenated. This allows the cobalt objects to pre-render upon creation
167+ which means that only changes have to be rendered again.
168+ possible way to do this is to use the stacklist as the base, each entry in there contains the
169+ full stack of tags for a part of the content
170+ externalise this list, make the entries immutable, and use this in the editor as the model
171+ then use something like react.js to only render changes in the browser dom
172+ */
173+ //1: create a relative offsets list of annotation start and end points
174+ var relativeList = getRelativeList ( fragment . annotations ) ;
175+ //2: for each point in the relative offsets list, create a valid stack of html tags
176+ var stackList = getStackList ( relativeList ) ;
177+ //3: merge content and html tags
178+ return mergeContentAndTags ( fragment . content , stackList ) ;
35179 } ;
36-
37- html . cobaltToDom = function ( fragment , dom ) {
38-
39- } ;
40-
41- html . cobaltDomToHtmlDom = function ( relativeFragment , dom ) {
42- // cobalt fragment with relative offsets and links to html nodes to html dom
43- // cobalt dom serves as a shadow dom for the html dom
44- } ;
45-
180+
46181 html . htmlToCobalt = function ( htmlString ) {
47-
182+
48183 return fragment ;
49184 } ;
50-
185+
186+ html . escapeContent = function ( content ) {
187+ return content
188+ . replace ( / & / g, "&" )
189+ . replace ( / < / g, "<" )
190+ . replace ( / > / g, ">" )
191+ . replace ( / " / g, """ )
192+ . replace ( / ' / g, "'" ) ;
193+ } ;
194+
51195} ) ( cobalt . html || { } ) ;
0 commit comments