fck_replace.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
  2. <!--
  3. * FCKeditor - The text editor for Internet - http://www.fckeditor.net
  4. * Copyright (C) 2003-2010 Frederico Caldeira Knabben
  5. *
  6. * == BEGIN LICENSE ==
  7. *
  8. * Licensed under the terms of any of the following licenses at your
  9. * choice:
  10. *
  11. * - GNU General Public License Version 2 or later (the "GPL")
  12. * http://www.gnu.org/licenses/gpl.html
  13. *
  14. * - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
  15. * http://www.gnu.org/licenses/lgpl.html
  16. *
  17. * - Mozilla Public License Version 1.1 or later (the "MPL")
  18. * http://www.mozilla.org/MPL/MPL-1.1.html
  19. *
  20. * == END LICENSE ==
  21. *
  22. * "Find" and "Replace" dialog box window.
  23. -->
  24. <html xmlns="http://www.w3.org/1999/xhtml">
  25. <head>
  26. <title></title>
  27. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  28. <meta content="noindex, nofollow" name="robots" />
  29. <script src="common/fck_dialog_common.js" type="text/javascript"></script>
  30. <script type="text/javascript">
  31. var dialog = window.parent ;
  32. var oEditor = dialog.InnerDialogLoaded() ;
  33. var dialogArguments = dialog.Args() ;
  34. var FCKLang = oEditor.FCKLang ;
  35. var FCKDomTools = oEditor.FCKDomTools ;
  36. var FCKDomRange = oEditor.FCKDomRange ;
  37. var FCKListsLib = oEditor.FCKListsLib ;
  38. var FCKTools = oEditor.FCKTools ;
  39. var EditorDocument = oEditor.FCK.EditorDocument ;
  40. var HighlightStyle = oEditor.FCKStyles.GetStyle( '_FCK_SelectionHighlight' ) ;
  41. dialog.AddTab( 'Find', FCKLang.DlgFindTitle ) ;
  42. dialog.AddTab( 'Replace', FCKLang.DlgReplaceTitle ) ;
  43. var idMap = {} ;
  44. function OnDialogTabChange( tabCode )
  45. {
  46. ShowE( 'divFind', ( tabCode == 'Find' ) ) ;
  47. ShowE( 'divReplace', ( tabCode == 'Replace' ) ) ;
  48. idMap['FindText'] = 'txtFind' + tabCode ;
  49. idMap['CheckCase'] = 'chkCase' + tabCode ;
  50. idMap['CheckWord'] = 'chkWord' + tabCode ;
  51. if ( tabCode == 'Replace' )
  52. dialog.SetAutoSize( true ) ;
  53. }
  54. GetNextNonEmptyTextNode = function( node, stopNode )
  55. {
  56. while ( ( node = FCKDomTools.GetNextSourceNode( node, false, 3, stopNode ) ) && node && node.length < 1 )
  57. 1 ;
  58. return node ;
  59. }
  60. CharacterCursor = function( arg )
  61. {
  62. if ( arg.nodeType && arg.nodeType == 9 )
  63. {
  64. this._textNode = GetNextNonEmptyTextNode( arg.body, arg.documentElement ) ;
  65. this._offset = 0 ;
  66. this._doc = arg ;
  67. }
  68. else
  69. {
  70. this._textNode = arguments[0] ;
  71. this._offset = arguments[1] ;
  72. this._doc = FCKTools.GetElementDocument( arguments[0] ) ;
  73. }
  74. }
  75. CharacterCursor.prototype =
  76. {
  77. GetCharacter : function()
  78. {
  79. return ( this._textNode && this._textNode.nodeValue.charAt( this._offset ) ) || null ;
  80. },
  81. // Non-normalized.
  82. GetTextNode : function()
  83. {
  84. return this._textNode ;
  85. },
  86. // Non-normalized.
  87. GetIndex : function()
  88. {
  89. return this._offset ;
  90. },
  91. // Return value means whehther we've crossed a line break or a paragraph boundary.
  92. MoveNext : function()
  93. {
  94. if ( this._offset < this._textNode.length - 1 )
  95. {
  96. this._offset++ ;
  97. return false ;
  98. }
  99. var crossed = false ;
  100. var curNode = this._textNode ;
  101. while ( ( curNode = FCKDomTools.GetNextSourceNode( curNode ) )
  102. && curNode && ( curNode.nodeType != 3 || curNode.length < 1 ) )
  103. {
  104. var tag = curNode.nodeName.toLowerCase() ;
  105. if ( FCKListsLib.BlockElements[tag] || tag == 'br' )
  106. crossed = true ;
  107. }
  108. this._textNode = curNode ;
  109. this._offset = 0 ;
  110. return crossed ;
  111. },
  112. // Return value means whehther we've crossed a line break or a paragraph boundary.
  113. MoveBack : function()
  114. {
  115. if ( this._offset > 0 && this._textNode.length > 0 )
  116. {
  117. this._offset = Math.min( this._offset - 1, this._textNode.length - 1 ) ;
  118. return false ;
  119. }
  120. var crossed = false ;
  121. var curNode = this._textNode ;
  122. while ( ( curNode = FCKDomTools.GetPreviousSourceNode( curNode ) )
  123. && curNode && ( curNode.nodeType != 3 || curNode.length < 1 ) )
  124. {
  125. var tag = curNode.nodeName.toLowerCase() ;
  126. if ( FCKListsLib.BlockElements[tag] || tag == 'br' )
  127. crossed = true ;
  128. }
  129. this._textNode = curNode ;
  130. this._offset = curNode && curNode.length - 1 ;
  131. return crossed ;
  132. },
  133. Clone : function()
  134. {
  135. return new CharacterCursor( this._textNode, this._offset ) ;
  136. }
  137. } ;
  138. CharacterRange = function( initCursor, maxLength )
  139. {
  140. this._cursors = initCursor.push ? initCursor : [initCursor] ;
  141. this._maxLength = maxLength ;
  142. this._highlightRange = null ;
  143. }
  144. CharacterRange.prototype =
  145. {
  146. ToDomRange : function()
  147. {
  148. var firstCursor = this._cursors[0] ;
  149. var lastCursor = this._cursors[ this._cursors.length - 1 ] ;
  150. var domRange = new FCKDomRange( FCKTools.GetElementWindow( firstCursor.GetTextNode() ) ) ;
  151. var w3cRange = domRange._Range = domRange.CreateRange() ;
  152. w3cRange.setStart( firstCursor.GetTextNode(), firstCursor.GetIndex() ) ;
  153. w3cRange.setEnd( lastCursor.GetTextNode(), lastCursor.GetIndex() + 1 ) ;
  154. domRange._UpdateElementInfo() ;
  155. return domRange ;
  156. },
  157. Highlight : function()
  158. {
  159. if ( this._cursors.length < 1 )
  160. return ;
  161. var domRange = this.ToDomRange() ;
  162. HighlightStyle.ApplyToRange( domRange, false, true ) ;
  163. this._highlightRange = domRange ;
  164. var charRange = CharacterRange.CreateFromDomRange( domRange ) ;
  165. var focusNode = domRange.StartNode ;
  166. if ( focusNode.nodeType != 1 )
  167. focusNode = focusNode.parentNode ;
  168. FCKDomTools.ScrollIntoView( focusNode, false ) ;
  169. this._cursors = charRange._cursors ;
  170. },
  171. RemoveHighlight : function()
  172. {
  173. if ( this._highlightRange )
  174. {
  175. HighlightStyle.RemoveFromRange( this._highlightRange, false, true ) ;
  176. var charRange = CharacterRange.CreateFromDomRange( this._highlightRange ) ;
  177. this._cursors = charRange._cursors ;
  178. this._highlightRange = null ;
  179. }
  180. },
  181. GetHighlightDomRange : function()
  182. {
  183. return this._highlightRange;
  184. },
  185. MoveNext : function()
  186. {
  187. var next = this._cursors[ this._cursors.length - 1 ].Clone() ;
  188. var retval = next.MoveNext() ;
  189. if ( retval )
  190. this._cursors = [] ;
  191. this._cursors.push( next ) ;
  192. if ( this._cursors.length > this._maxLength )
  193. this._cursors.shift() ;
  194. return retval ;
  195. },
  196. MoveBack : function()
  197. {
  198. var prev = this._cursors[0].Clone() ;
  199. var retval = prev.MoveBack() ;
  200. if ( retval )
  201. this._cursors = [] ;
  202. this._cursors.unshift( prev ) ;
  203. if ( this._cursors.length > this._maxLength )
  204. this._cursors.pop() ;
  205. return retval ;
  206. },
  207. GetEndCharacter : function()
  208. {
  209. if ( this._cursors.length < 1 )
  210. return null ;
  211. var retval = this._cursors[ this._cursors.length - 1 ].GetCharacter() ;
  212. return retval ;
  213. },
  214. GetNextRange : function( len )
  215. {
  216. if ( this._cursors.length == 0 )
  217. return null ;
  218. var cur = this._cursors[ this._cursors.length - 1 ].Clone() ;
  219. cur.MoveNext() ;
  220. return new CharacterRange( cur, len ) ;
  221. },
  222. GetCursors : function()
  223. {
  224. return this._cursors ;
  225. }
  226. } ;
  227. CharacterRange.CreateFromDomRange = function( domRange )
  228. {
  229. var w3cRange = domRange._Range ;
  230. var startContainer = w3cRange.startContainer ;
  231. var endContainer = w3cRange.endContainer ;
  232. var startTextNode, startIndex, endTextNode, endIndex ;
  233. if ( startContainer.nodeType == 3 )
  234. {
  235. startTextNode = startContainer ;
  236. startIndex = w3cRange.startOffset ;
  237. }
  238. else if ( domRange.StartNode.nodeType == 3 )
  239. {
  240. startTextNode = domRange.StartNode ;
  241. startIndex = 0 ;
  242. }
  243. else
  244. {
  245. startTextNode = GetNextNonEmptyTextNode( domRange.StartNode, domRange.StartNode.parentNode ) ;
  246. if ( !startTextNode )
  247. return null ;
  248. startIndex = 0 ;
  249. }
  250. if ( endContainer.nodeType == 3 && w3cRange.endOffset > 0 )
  251. {
  252. endTextNode = endContainer ;
  253. endIndex = w3cRange.endOffset - 1 ;
  254. }
  255. else
  256. {
  257. endTextNode = domRange.EndNode ;
  258. while ( endTextNode.nodeType != 3 )
  259. endTextNode = endTextNode.lastChild ;
  260. endIndex = endTextNode.length - 1 ;
  261. }
  262. var cursors = [] ;
  263. var current = new CharacterCursor( startTextNode, startIndex ) ;
  264. cursors.push( current ) ;
  265. if ( !( current.GetTextNode() == endTextNode && current.GetIndex() == endIndex ) && !domRange.CheckIsEmpty() )
  266. {
  267. do
  268. {
  269. current = current.Clone() ;
  270. current.MoveNext() ;
  271. cursors.push( current ) ;
  272. }
  273. while ( !( current.GetTextNode() == endTextNode && current.GetIndex() == endIndex ) ) ;
  274. }
  275. return new CharacterRange( cursors, cursors.length ) ;
  276. }
  277. // Knuth-Morris-Pratt Algorithm for stream input
  278. KMP_NOMATCH = 0 ;
  279. KMP_ADVANCED = 1 ;
  280. KMP_MATCHED = 2 ;
  281. KmpMatch = function( pattern, ignoreCase )
  282. {
  283. var overlap = [ -1 ] ;
  284. for ( var i = 0 ; i < pattern.length ; i++ )
  285. {
  286. overlap.push( overlap[i] + 1 ) ;
  287. while ( overlap[ i + 1 ] > 0 && pattern.charAt( i ) != pattern.charAt( overlap[ i + 1 ] - 1 ) )
  288. overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1 ;
  289. }
  290. this._Overlap = overlap ;
  291. this._State = 0 ;
  292. this._IgnoreCase = ( ignoreCase === true ) ;
  293. if ( ignoreCase )
  294. this.Pattern = pattern.toLowerCase();
  295. else
  296. this.Pattern = pattern ;
  297. }
  298. KmpMatch.prototype = {
  299. FeedCharacter : function( c )
  300. {
  301. if ( this._IgnoreCase )
  302. c = c.toLowerCase();
  303. while ( true )
  304. {
  305. if ( c == this.Pattern.charAt( this._State ) )
  306. {
  307. this._State++ ;
  308. if ( this._State == this.Pattern.length )
  309. {
  310. // found a match, start over, don't care about partial matches involving the current match
  311. this._State = 0;
  312. return KMP_MATCHED;
  313. }
  314. return KMP_ADVANCED ;
  315. }
  316. else if ( this._State == 0 )
  317. return KMP_NOMATCH;
  318. else
  319. this._State = this._Overlap[ this._State ];
  320. }
  321. return null ;
  322. },
  323. Reset : function()
  324. {
  325. this._State = 0 ;
  326. }
  327. };
  328. // Place a range at the start of document.
  329. function OnLoad()
  330. {
  331. // First of all, translate the dialog box texts.
  332. oEditor.FCKLanguageManager.TranslatePage( document ) ;
  333. // Show the appropriate tab at startup.
  334. if ( dialogArguments.CustomValue == 'Find' )
  335. {
  336. dialog.SetSelectedTab( 'Find' ) ;
  337. dialog.SetAutoSize( true ) ;
  338. }
  339. else
  340. dialog.SetSelectedTab( 'Replace' ) ;
  341. SelectField( 'txtFind' + dialogArguments.CustomValue ) ;
  342. }
  343. function btnStat()
  344. {
  345. GetE('btnReplace').disabled =
  346. GetE('btnReplaceAll').disabled =
  347. GetE('btnFind').disabled =
  348. ( GetE(idMap["FindText"]).value.length == 0 ) ;
  349. }
  350. function btnStatDelayed()
  351. {
  352. setTimeout( btnStat, 1 ) ;
  353. }
  354. function GetSearchString()
  355. {
  356. return GetE(idMap['FindText']).value ;
  357. }
  358. function GetReplaceString()
  359. {
  360. return GetE("txtReplace").value ;
  361. }
  362. function GetCheckCase()
  363. {
  364. return !! ( GetE(idMap['CheckCase']).checked ) ;
  365. }
  366. function GetMatchWord()
  367. {
  368. return !! ( GetE(idMap['CheckWord']).checked ) ;
  369. }
  370. /* Is this character a unicode whitespace or a punctuation mark?
  371. * References:
  372. * http://unicode.org/Public/UNIDATA/PropList.txt (whitespaces)
  373. * http://php.chinaunix.net/manual/tw/ref.regex.php (punctuation marks)
  374. */
  375. function CheckIsWordSeparator( c )
  376. {
  377. if ( !c )
  378. return true;
  379. var code = c.charCodeAt( 0 );
  380. if ( code >= 9 && code <= 0xd )
  381. return true;
  382. if ( code >= 0x2000 && code <= 0x200a )
  383. return true;
  384. switch ( code )
  385. {
  386. case 0x20:
  387. case 0x85:
  388. case 0xa0:
  389. case 0x1680:
  390. case 0x180e:
  391. case 0x2028:
  392. case 0x2029:
  393. case 0x202f:
  394. case 0x205f:
  395. case 0x3000:
  396. return true;
  397. default:
  398. }
  399. return /[.,"'?!;:]/.test( c ) ;
  400. }
  401. FindRange = null ;
  402. function _Find()
  403. {
  404. var searchString = GetSearchString() ;
  405. if ( !FindRange )
  406. FindRange = new CharacterRange( new CharacterCursor( EditorDocument ), searchString.length ) ;
  407. else
  408. {
  409. FindRange.RemoveHighlight() ;
  410. FindRange = FindRange.GetNextRange( searchString.length ) ;
  411. }
  412. var matcher = new KmpMatch( searchString, ! GetCheckCase() ) ;
  413. var matchState = KMP_NOMATCH ;
  414. var character = '%' ;
  415. while ( character != null )
  416. {
  417. while ( ( character = FindRange.GetEndCharacter() ) )
  418. {
  419. matchState = matcher.FeedCharacter( character ) ;
  420. if ( matchState == KMP_MATCHED )
  421. break ;
  422. if ( FindRange.MoveNext() )
  423. matcher.Reset() ;
  424. }
  425. if ( matchState == KMP_MATCHED )
  426. {
  427. if ( GetMatchWord() )
  428. {
  429. var cursors = FindRange.GetCursors() ;
  430. var head = cursors[ cursors.length - 1 ].Clone() ;
  431. var tail = cursors[0].Clone() ;
  432. if ( !head.MoveNext() && !CheckIsWordSeparator( head.GetCharacter() ) )
  433. continue ;
  434. if ( !tail.MoveBack() && !CheckIsWordSeparator( tail.GetCharacter() ) )
  435. continue ;
  436. }
  437. FindRange.Highlight() ;
  438. return true ;
  439. }
  440. }
  441. FindRange = null ;
  442. return false ;
  443. }
  444. function Find()
  445. {
  446. if ( ! _Find() )
  447. alert( FCKLang.DlgFindNotFoundMsg ) ;
  448. }
  449. function Replace()
  450. {
  451. var saveUndoStep = function( selectRange )
  452. {
  453. var ieRange ;
  454. if ( oEditor.FCKBrowserInfo.IsIE )
  455. ieRange = document.selection.createRange() ;
  456. selectRange.Select() ;
  457. oEditor.FCKUndo.SaveUndoStep() ;
  458. var cloneRange = selectRange.Clone() ;
  459. cloneRange.Collapse( false ) ;
  460. cloneRange.Select() ;
  461. if ( ieRange )
  462. setTimeout( function(){ ieRange.select() ; }, 1 ) ;
  463. }
  464. if ( FindRange && FindRange.GetHighlightDomRange() )
  465. {
  466. var range = FindRange.GetHighlightDomRange() ;
  467. var bookmark = range.CreateBookmark() ;
  468. FindRange.RemoveHighlight() ;
  469. range.MoveToBookmark( bookmark ) ;
  470. saveUndoStep( range ) ;
  471. range.DeleteContents() ;
  472. range.InsertNode( EditorDocument.createTextNode( GetReplaceString() ) ) ;
  473. range._UpdateElementInfo() ;
  474. FindRange = CharacterRange.CreateFromDomRange( range ) ;
  475. }
  476. else
  477. {
  478. if ( ! _Find() )
  479. {
  480. FindRange && FindRange.RemoveHighlight() ;
  481. alert( FCKLang.DlgFindNotFoundMsg ) ;
  482. }
  483. }
  484. }
  485. function ReplaceAll()
  486. {
  487. oEditor.FCKUndo.SaveUndoStep() ;
  488. var replaceCount = 0 ;
  489. while ( _Find() )
  490. {
  491. var range = FindRange.GetHighlightDomRange() ;
  492. var bookmark = range.CreateBookmark() ;
  493. FindRange.RemoveHighlight() ;
  494. range.MoveToBookmark( bookmark) ;
  495. range.DeleteContents() ;
  496. range.InsertNode( EditorDocument.createTextNode( GetReplaceString() ) ) ;
  497. range._UpdateElementInfo() ;
  498. FindRange = CharacterRange.CreateFromDomRange( range ) ;
  499. replaceCount++ ;
  500. }
  501. if ( replaceCount == 0 )
  502. {
  503. FindRange && FindRange.RemoveHighlight() ;
  504. alert( FCKLang.DlgFindNotFoundMsg ) ;
  505. }
  506. dialog.Cancel() ;
  507. }
  508. window.onunload = function()
  509. {
  510. if ( FindRange )
  511. {
  512. FindRange.RemoveHighlight() ;
  513. FindRange.ToDomRange().Select() ;
  514. }
  515. }
  516. </script>
  517. <style type="text/css">
  518. body, td, input, textarea, select, label { font-family: Arial, Verdana, Geneva, helvetica, sans-serif; font-size: 11px; }
  519. </style>
  520. </head>
  521. <body onload="OnLoad()" style="overflow: hidden">
  522. <div id="divFind" style="display: none">
  523. <table cellspacing="3" cellpadding="2" width="100%" border="0">
  524. <tr>
  525. <td nowrap="nowrap">
  526. <label for="txtFindFind" fcklang="DlgReplaceFindLbl">
  527. Find what:</label>
  528. </td>
  529. <td width="100%">
  530. <input id="txtFindFind" onkeyup="btnStat()" oninput="btnStat()" onpaste="btnStatDelayed()" style="width: 100%" tabindex="1"
  531. type="text" />
  532. </td>
  533. <td>
  534. <input id="btnFind" style="width: 80px" disabled="disabled" onclick="Find();"
  535. type="button" value="Find" fcklang="DlgFindFindBtn" />
  536. </td>
  537. </tr>
  538. <tr>
  539. <td valign="bottom" colspan="3">
  540. &nbsp;<input id="chkCaseFind" tabindex="3" type="checkbox" /><label for="chkCaseFind" fcklang="DlgReplaceCaseChk">Match
  541. case</label>
  542. <br />
  543. &nbsp;<input id="chkWordFind" tabindex="4" type="checkbox" /><label for="chkWordFind" fcklang="DlgReplaceWordChk">Match
  544. whole word</label>
  545. </td>
  546. </tr>
  547. </table>
  548. </div>
  549. <div id="divReplace" style="display:none">
  550. <table cellspacing="3" cellpadding="2" width="100%" border="0">
  551. <tr>
  552. <td nowrap="nowrap">
  553. <label for="txtFindReplace" fcklang="DlgReplaceFindLbl">
  554. Find what:</label>
  555. </td>
  556. <td width="100%">
  557. <input id="txtFindReplace" onkeyup="btnStat()" oninput="btnStat()" onpaste="btnStatDelayed()" style="width: 100%" tabindex="1"
  558. type="text" />
  559. </td>
  560. <td>
  561. <input id="btnReplace" style="width: 80px" disabled="disabled" onclick="Replace();"
  562. type="button" value="Replace" fcklang="DlgReplaceReplaceBtn" />
  563. </td>
  564. </tr>
  565. <tr>
  566. <td valign="top" nowrap="nowrap">
  567. <label for="txtReplace" fcklang="DlgReplaceReplaceLbl">
  568. Replace with:</label>
  569. </td>
  570. <td valign="top">
  571. <input id="txtReplace" style="width: 100%" tabindex="2" type="text" />
  572. </td>
  573. <td>
  574. <input id="btnReplaceAll" style="width: 80px" disabled="disabled" onclick="ReplaceAll()" type="button"
  575. value="Replace All" fcklang="DlgReplaceReplAllBtn" />
  576. </td>
  577. </tr>
  578. <tr>
  579. <td valign="bottom" colspan="3">
  580. &nbsp;<input id="chkCaseReplace" tabindex="3" type="checkbox" /><label for="chkCaseReplace" fcklang="DlgReplaceCaseChk">Match
  581. case</label>
  582. <br />
  583. &nbsp;<input id="chkWordReplace" tabindex="4" type="checkbox" /><label for="chkWordReplace" fcklang="DlgReplaceWordChk">Match
  584. whole word</label>
  585. </td>
  586. </tr>
  587. </table>
  588. </div>
  589. </body>
  590. </html>