Fix Tidy parse error inside script tag

Due to the fact that there is not any other html cleaner in native code better than Tidy now. However, there is a big problem that is Tidy parse wrongly html code like this:

  1.         <script type="text/javascript">
  2.         //<![CDATA[
  3.         try{if (!window.CloudFlare) {var CloudFlare=[{verbose:0,p:0,byc:0,owlid:"cf",bag2:1,mirage2:0,oracle:0,paths:{cloudflare:"/cdn-cgi/nexp/dok2v=1613a3a185/"},atok:"4efbb71447e53487d255646482b289d3",petok:"2c972b6b8a940a09d6cc73fd0e44af4e89f897a5-1413435957-1800",zone:"ngaytho.info",rocket:"a",apps:0}];document.write('<script type="text/javascript" src="//ajax.cloudflare.com/cdn-cgi/nexp/dok2v=919620257c/cloudflare.min.js"><'+'\/script>');}}catch(e){};
  4.         //]]>
  5.         </script>
Hide/show line number

Tidy try to preserve html tag inside script so it still try to parse whatever in script tag (Ex: <head><script src=foo><meta name=foo content=bar>). So the code above make Tidy wrong because the of the string '<script type="text/javascript" src="//ajax.cloudflare.com/cdn-cgi/nexp/dok2v=919620257c/cloudflare.min.js"><'. Tidy can not detect which is string in script and which is preservable tag in script. Therefore, to fix this error I have to disable in script tag preserve function by adding a condition to Tidy source code.

This is original GetCDATA function of Tidy source code (our example follow this project https://github.com/w3c/tidy-html5):

lexer.c
  1. static Node *GetCDATA( TidyDocImpl* doc, Node *container )
  2. {
  3.     Lexer* lexer = doc->lexer;
  4.     uint start = 0;
  5.     int nested = 0;
  6.     CDATAState state = CDATA_INTERMEDIATE;
  7.     uint i;
  8.     Bool isEmpty = yes;
  9.     Bool matches = no;
  10.     uint c;
  11.     Bool hasSrc = TY_(AttrGetById)(container, TidyAttr_SRC) != NULL;
  12.  
  13.     SetLexerLocus( doc, lexer );
  14.     lexer->waswhite = no;
  15.     lexer->txtstart = lexer->txtend = lexer->lexsize;
  16.  
  17.     /* seen start tag, look for matching end tag */
  18.     while ((c = TY_(ReadChar)(doc->docIn)) != EndOfStream)
  19.     {
  20.         TY_(AddCharToLexer)(lexer, c);
  21.         lexer->txtend = lexer->lexsize;
  22.  
  23.         if (state == CDATA_INTERMEDIATE)
  24.         {
  25.             if (c != '<')
  26.             {
  27.                 if (isEmpty && !TY_(IsWhite)(c))
  28.                     isEmpty = no;
  29.                 continue;
  30.             }
  31.  
  32.             c = TY_(ReadChar)(doc->docIn);
  33.  
  34.             if (TY_(IsLetter)(c))
  35.             {
  36.                 /* <head><script src=foo><meta name=foo content=bar>*/
  37.                 if (hasSrc && isEmpty && nodeIsSCRIPT(container))
  38.                 {
  39.                     /* ReportError(doc, container, NULL, MISSING_ENDTAG_FOR); */
  40.                     lexer->lexsize = lexer->txtstart;
  41.                     TY_(UngetChar)(c, doc->docIn);
  42.                     TY_(UngetChar)('<', doc->docIn);
  43.                     return NULL;
  44.                 }
  45.                 TY_(AddCharToLexer)(lexer, c);
  46.                 start = lexer->lexsize - 1;
  47.                 state = CDATA_STARTTAG;
  48.             }
  49.             else if (c == '/')
  50.             {
  51.                 TY_(AddCharToLexer)(lexer, c);
  52.  
  53.                 c = TY_(ReadChar)(doc->docIn);
  54.  
  55.                 if (!TY_(IsLetter)(c))
  56.                 {
  57.                     TY_(UngetChar)(c, doc->docIn);
  58.                     continue;
  59.                 }
  60.                 TY_(UngetChar)(c, doc->docIn);
  61.  
  62.                 start = lexer->lexsize;
  63.                 state = CDATA_ENDTAG;
  64.             }
  65.             else if (c == '\\')
  66.             {
  67.                 /* recognize document.write("<script><\/script>") */
  68.                 TY_(AddCharToLexer)(lexer, c);
  69.  
  70.                 c = TY_(ReadChar)(doc->docIn);
  71.  
  72.                 if (c != '/')
  73.                 {
  74.                     TY_(UngetChar)(c, doc->docIn);
  75.                     continue;
  76.                 }
  77.  
  78.                 TY_(AddCharToLexer)(lexer, c);
  79.                 c = TY_(ReadChar)(doc->docIn);
  80.  
  81.                 if (!TY_(IsLetter)(c))
  82.                 {
  83.                     TY_(UngetChar)(c, doc->docIn);
  84.                     continue;
  85.                 }
  86.                 TY_(UngetChar)(c, doc->docIn);
  87.  
  88.                 start = lexer->lexsize;
  89.                 state = CDATA_ENDTAG;
  90.             }
  91.             else
  92.             {
  93.                 TY_(UngetChar)(c, doc->docIn);
  94.             }
  95.         }
  96.         /* '<' + Letter found */
  97.         else if (state == CDATA_STARTTAG)
  98.         {
  99.             if (TY_(IsLetter)(c))
  100.                 continue;
  101.  
  102.             matches = TY_(tmbstrncasecmp)(container->element, lexer->lexbuf + start,
  103.                                           TY_(tmbstrlen)(container->element)) == 0;
  104.             if (matches)
  105.                 nested++;
  106.  
  107.             state = CDATA_INTERMEDIATE;
  108.         }
  109.         /* '<' + '/' + Letter found */
  110.         else if (state == CDATA_ENDTAG)
  111.         {
  112.             if (TY_(IsLetter)(c))
  113.                 continue;
  114.  
  115.             matches = TY_(tmbstrncasecmp)(container->element, lexer->lexbuf + start,
  116.                                           TY_(tmbstrlen)(container->element)) == 0;
  117.  
  118.             if (isEmpty && !matches)
  119.             {
  120.                 /* ReportError(doc, container, NULL, MISSING_ENDTAG_FOR); */
  121.  
  122.                 for (i = lexer->lexsize - 1; i >= start; --i)
  123.                     TY_(UngetChar)((uint)lexer->lexbuf[i], doc->docIn);
  124.                 TY_(UngetChar)('/', doc->docIn);
  125.                 TY_(UngetChar)('<', doc->docIn);
  126.                 break;
  127.             }
  128.  
  129.             if (matches && nested-- <= 0)
  130.             {
  131.                 for (i = lexer->lexsize - 1; i >= start; --i)
  132.                     TY_(UngetChar)((uint)lexer->lexbuf[i], doc->docIn);
  133.                 TY_(UngetChar)('/', doc->docIn);
  134.                 TY_(UngetChar)('<', doc->docIn);
  135.                 lexer->lexsize -= (lexer->lexsize - start) + 2;
  136.                 break;
  137.             }
  138.             else if (lexer->lexbuf[start - 2] != '\\')
  139.             {
  140.                 /* if the end tag is not already escaped using backslash */
  141.                 SetLexerLocus( doc, lexer );
  142.                 lexer->columns -= 3;
  143.                 TY_(ReportError)(doc, NULL, NULL, BAD_CDATA_CONTENT);
  144.  
  145.                 /* if javascript insert backslash before / */
  146.                 if (TY_(IsJavaScript)(container))
  147.                 {
  148.                     for (i = lexer->lexsize; i > start-1; --i)
  149.                         lexer->lexbuf[i] = lexer->lexbuf[i-1];
  150.  
  151.                     lexer->lexbuf[start-1] = '\\';
  152.                     lexer->lexsize++;
  153.                 }
  154.             }
  155.             state = CDATA_INTERMEDIATE;
  156.         }
  157.     }
  158.     if (isEmpty)
  159.         lexer->lexsize = lexer->txtstart = lexer->txtend;
  160.     else
  161.         lexer->txtend = lexer->lexsize;
  162.  
  163.     if (c == EndOfStream)
  164.         TY_(ReportError)(doc, container, NULL, MISSING_ENDTAG_FOR );
  165.  
  166. /* this was disabled for some reason... */
  167. #if 0
  168.     if (lexer->txtend > lexer->txtstart)
  169.         return TextToken(lexer);
  170.     else
  171.         return NULL;
  172. #else
  173.     return TY_(TextToken)(lexer);
  174. #endif
  175. }
Hide/show line number

We add a contdition into if statement to prevent Tidy from process the html tag inside script. We have not to delete that code because that code also use to parse CDATA element. Our new GetCDATA become (change only at line 34):

lexer.c
  1. static Node *GetCDATA( TidyDocImpl* doc, Node *container )
  2. {
  3.     Lexer* lexer = doc->lexer;
  4.     uint start = 0;
  5.     int nested = 0;
  6.     CDATAState state = CDATA_INTERMEDIATE;
  7.     uint i;
  8.     Bool isEmpty = yes;
  9.     Bool matches = no;
  10.     uint c;
  11.     Bool hasSrc = TY_(AttrGetById)(container, TidyAttr_SRC) != NULL;
  12.  
  13.     SetLexerLocus( doc, lexer );
  14.     lexer->waswhite = no;
  15.     lexer->txtstart = lexer->txtend = lexer->lexsize;
  16.  
  17.     /* seen start tag, look for matching end tag */
  18.     while ((c = TY_(ReadChar)(doc->docIn)) != EndOfStream)
  19.     {
  20.         TY_(AddCharToLexer)(lexer, c);
  21.         lexer->txtend = lexer->lexsize;
  22.  
  23.         if (state == CDATA_INTERMEDIATE)
  24.         {
  25.             if (c != '<')
  26.             {
  27.                 if (isEmpty && !TY_(IsWhite)(c))
  28.                     isEmpty = no;
  29.                 continue;
  30.             }
  31.  
  32.             c = TY_(ReadChar)(doc->docIn);
  33.  
  34.             if (TY_(IsLetter)(c) && !nodeIsSCRIPT(container))
  35.             {
  36.                 /* <head><script src=foo><meta name=foo content=bar>*/
  37.                 if (hasSrc && isEmpty && nodeIsSCRIPT(container))
  38.                 {
  39.                     /* ReportError(doc, container, NULL, MISSING_ENDTAG_FOR); */
  40.                     lexer->lexsize = lexer->txtstart;
  41.                     TY_(UngetChar)(c, doc->docIn);
  42.                     TY_(UngetChar)('<', doc->docIn);
  43.                     return NULL;
  44.                 }
  45.                 TY_(AddCharToLexer)(lexer, c);
  46.                 start = lexer->lexsize - 1;
  47.                 state = CDATA_STARTTAG;
  48.             }
  49.             else if (c == '/')
  50.             {
  51.                 TY_(AddCharToLexer)(lexer, c);
  52.  
  53.                 c = TY_(ReadChar)(doc->docIn);
  54.  
  55.                 if (!TY_(IsLetter)(c))
  56.                 {
  57.                     TY_(UngetChar)(c, doc->docIn);
  58.                     continue;
  59.                 }
  60.                 TY_(UngetChar)(c, doc->docIn);
  61.  
  62.                 start = lexer->lexsize;
  63.                 state = CDATA_ENDTAG;
  64.             }
  65.             else if (c == '\\')
  66.             {
  67.                 /* recognize document.write("<script><\/script>") */
  68.                 TY_(AddCharToLexer)(lexer, c);
  69.  
  70.                 c = TY_(ReadChar)(doc->docIn);
  71.  
  72.                 if (c != '/')
  73.                 {
  74.                     TY_(UngetChar)(c, doc->docIn);
  75.                     continue;
  76.                 }
  77.  
  78.                 TY_(AddCharToLexer)(lexer, c);
  79.                 c = TY_(ReadChar)(doc->docIn);
  80.  
  81.                 if (!TY_(IsLetter)(c))
  82.                 {
  83.                     TY_(UngetChar)(c, doc->docIn);
  84.                     continue;
  85.                 }
  86.                 TY_(UngetChar)(c, doc->docIn);
  87.  
  88.                 start = lexer->lexsize;
  89.                 state = CDATA_ENDTAG;
  90.             }
  91.             else
  92.             {
  93.                 TY_(UngetChar)(c, doc->docIn);
  94.             }
  95.         }
  96.         /* '<' + Letter found */
  97.         else if (state == CDATA_STARTTAG)
  98.         {
  99.             if (TY_(IsLetter)(c))
  100.                 continue;
  101.  
  102.             matches = TY_(tmbstrncasecmp)(container->element, lexer->lexbuf + start,
  103.                                           TY_(tmbstrlen)(container->element)) == 0;
  104.             if (matches)
  105.                 nested++;
  106.  
  107.             state = CDATA_INTERMEDIATE;
  108.         }
  109.         /* '<' + '/' + Letter found */
  110.         else if (state == CDATA_ENDTAG)
  111.         {
  112.             if (TY_(IsLetter)(c))
  113.                 continue;
  114.  
  115.             matches = TY_(tmbstrncasecmp)(container->element, lexer->lexbuf + start,
  116.                                           TY_(tmbstrlen)(container->element)) == 0;
  117.  
  118.             if (isEmpty && !matches)
  119.             {
  120.                 /* ReportError(doc, container, NULL, MISSING_ENDTAG_FOR); */
  121.  
  122.                 for (i = lexer->lexsize - 1; i >= start; --i)
  123.                     TY_(UngetChar)((uint)lexer->lexbuf[i], doc->docIn);
  124.                 TY_(UngetChar)('/', doc->docIn);
  125.                 TY_(UngetChar)('<', doc->docIn);
  126.                 break;
  127.             }
  128.  
  129.             if (matches && nested-- <= 0)
  130.             {
  131.                 for (i = lexer->lexsize - 1; i >= start; --i)
  132.                     TY_(UngetChar)((uint)lexer->lexbuf[i], doc->docIn);
  133.                 TY_(UngetChar)('/', doc->docIn);
  134.                 TY_(UngetChar)('<', doc->docIn);
  135.                 lexer->lexsize -= (lexer->lexsize - start) + 2;
  136.                 break;
  137.             }
  138.             else if (lexer->lexbuf[start - 2] != '\\')
  139.             {
  140.                 /* if the end tag is not already escaped using backslash */
  141.                 SetLexerLocus( doc, lexer );
  142.                 lexer->columns -= 3;
  143.                 TY_(ReportError)(doc, NULL, NULL, BAD_CDATA_CONTENT);
  144.  
  145.                 /* if javascript insert backslash before / */
  146.                 if (TY_(IsJavaScript)(container))
  147.                 {
  148.                     for (i = lexer->lexsize; i > start-1; --i)
  149.                         lexer->lexbuf[i] = lexer->lexbuf[i-1];
  150.  
  151.                     lexer->lexbuf[start-1] = '\\';
  152.                     lexer->lexsize++;
  153.                 }
  154.             }
  155.             state = CDATA_INTERMEDIATE;
  156.         }
  157.     }
  158.     if (isEmpty)
  159.         lexer->lexsize = lexer->txtstart = lexer->txtend;
  160.     else
  161.         lexer->txtend = lexer->lexsize;
  162.  
  163.     if (c == EndOfStream)
  164.         TY_(ReportError)(doc, container, NULL, MISSING_ENDTAG_FOR );
  165.  
  166. /* this was disabled for some reason... */
  167. #if 0
  168.     if (lexer->txtend > lexer->txtstart)
  169.         return TextToken(lexer);
  170.     else
  171.         return NULL;
  172. #else
  173.     return TY_(TextToken)(lexer);
  174. #endif
  175. }
Hide/show line number

As a result, Tidy parse everything inside script tag as text, so it work well with our problem.