index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. var ElementType = require("domelementtype");
  2. var re_whitespace = /\s+/g;
  3. function DomHandler(callback, options, elementCB){
  4. if(typeof callback === "object"){
  5. elementCB = options;
  6. options = callback;
  7. callback = null;
  8. } else if(typeof options === "function"){
  9. elementCB = options;
  10. options = defaultOpts;
  11. }
  12. this._callback = callback;
  13. this._options = options || defaultOpts;
  14. this._elementCB = elementCB;
  15. this.dom = [];
  16. this._done = false;
  17. this._tagStack = [];
  18. }
  19. //default options
  20. var defaultOpts = {
  21. normalizeWhitespace: false //Replace all whitespace with single spaces
  22. };
  23. //Resets the handler back to starting state
  24. DomHandler.prototype.onreset = function(){
  25. DomHandler.call(this, this._callback, this._options, this._elementCB);
  26. };
  27. //Signals the handler that parsing is done
  28. DomHandler.prototype.onend = function(){
  29. if(this._done) return;
  30. this._done = true;
  31. this._handleCallback(null);
  32. };
  33. DomHandler.prototype._handleCallback =
  34. DomHandler.prototype.onerror = function(error){
  35. if(typeof this._callback === "function"){
  36. this._callback(error, this.dom);
  37. } else {
  38. if(error) throw error;
  39. }
  40. };
  41. DomHandler.prototype.onclosetag = function(name){
  42. //if(this._tagStack.pop().name !== name) this._handleCallback(Error("Tagname didn't match!"));
  43. var elem = this._tagStack.pop();
  44. if(this._elementCB) this._elementCB(elem);
  45. };
  46. DomHandler.prototype._addDomElement = function(element){
  47. var lastTag = this._tagStack[this._tagStack.length - 1];
  48. if(lastTag){
  49. lastTag.children.push(element);
  50. } else { //There aren't parent elements
  51. this.dom.push(element);
  52. }
  53. };
  54. DomHandler.prototype.onopentag = function(name, attribs){
  55. var lastTag = this._tagStack[this._tagStack.length - 1];
  56. var element = {
  57. type: name === "script" ? ElementType.Script : name === "style" ? ElementType.Style : ElementType.Tag,
  58. name: name,
  59. attribs: attribs,
  60. children: [],
  61. prev: null,
  62. next: null,
  63. parent: lastTag || null
  64. };
  65. if(lastTag){
  66. var idx = lastTag.children.length;
  67. while(idx > 0){
  68. if(ElementType.isTag(lastTag.children[--idx])){
  69. element.prev = lastTag.children[idx];
  70. lastTag.children[idx].next = element;
  71. break;
  72. }
  73. }
  74. lastTag.children.push(element);
  75. } else {
  76. this.dom.push(element);
  77. }
  78. this._tagStack.push(element);
  79. };
  80. DomHandler.prototype.ontext = function(data){
  81. //the ignoreWhitespace is officially dropped, but for now,
  82. //it's an alias for normalizeWhitespace
  83. var normalize = this._options.normalizeWhitespace || this._options.ignoreWhitespace;
  84. var lastTag;
  85. if(!this._tagStack.length && this.dom.length && (lastTag = this.dom[this.dom.length-1]).type === ElementType.Text){
  86. if(normalize){
  87. lastTag.data = (lastTag.data + data).replace(re_whitespace, " ");
  88. } else {
  89. lastTag.data += data;
  90. }
  91. } else {
  92. if(
  93. this._tagStack.length &&
  94. (lastTag = this._tagStack[this._tagStack.length - 1]) &&
  95. (lastTag = lastTag.children[lastTag.children.length - 1]) &&
  96. lastTag.type === ElementType.Text
  97. ){
  98. if(normalize){
  99. lastTag.data = (lastTag.data + data).replace(re_whitespace, " ");
  100. } else {
  101. lastTag.data += data;
  102. }
  103. } else {
  104. if(normalize){
  105. data = data.replace(re_whitespace, " ");
  106. }
  107. this._addDomElement({
  108. data: data,
  109. type: ElementType.Text
  110. });
  111. }
  112. }
  113. };
  114. DomHandler.prototype.oncomment = function(data){
  115. var lastTag = this._tagStack[this._tagStack.length - 1];
  116. if(lastTag && lastTag.type === ElementType.Comment){
  117. lastTag.data += data;
  118. return;
  119. }
  120. var element = {
  121. data: data,
  122. type: ElementType.Comment
  123. };
  124. this._addDomElement(element);
  125. this._tagStack.push(element);
  126. };
  127. DomHandler.prototype.oncdatastart = function(){
  128. var element = {
  129. children: [{
  130. data: "",
  131. type: ElementType.Text
  132. }],
  133. type: ElementType.CDATA
  134. };
  135. this._addDomElement(element);
  136. this._tagStack.push(element);
  137. };
  138. DomHandler.prototype.oncommentend = DomHandler.prototype.oncdataend = function(){
  139. this._tagStack.pop();
  140. };
  141. DomHandler.prototype.onprocessinginstruction = function(name, data){
  142. this._addDomElement({
  143. name: name,
  144. data: data,
  145. type: ElementType.Directive
  146. });
  147. };
  148. module.exports = DomHandler;