mdict-renderer.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. //define(['jquery', 'bluebird', 'speex', 'pcmdata', 'bitstring'], factory);
  2. //$, Promise, SpeexLib, PCMDataLib
  3. /**
  4. * Usage:
  5. * var fileList = ...; // FileList object
  6. * var word = ...; // word for lookup
  7. * require(['mdict-parser', 'mdict-renderer'], function(MParser, MRenderer) {
  8. * MParser(fileList).then(function(resources) {
  9. * var mdict = MRenderer(resources),
  10. * dict_desc = resources.description.mdx;
  11. * mdict.lookup(word).then(function($content) {
  12. * // use $content to display result
  13. * });
  14. * });
  15. * });
  16. */
  17. var MRenderer = (function () {
  18. var MIME = {
  19. 'css': 'text/css',
  20. 'img': 'image',
  21. 'jpg': 'image/jpeg',
  22. 'png': 'image/png',
  23. 'spx': 'audio/x-speex',
  24. 'wav': 'audio/wav',
  25. 'mp3': 'audio/mp3',
  26. 'js' : 'text/javascript'
  27. };
  28. function getExtension(filename, defaultExt) {
  29. return /(?:\.([^.]+))?$/.exec(filename)[1] || defaultExt;
  30. }
  31. // TODO: revoke unused resource, LRU
  32. // TODO: support for word variation
  33. return function createRenderer(resources) {
  34. var cache = (function createCache(mdd) {
  35. var repo = {};
  36. function get(id, load) {
  37. var entry = repo[id];
  38. if (!entry) {
  39. repo[id] = entry = new Promise(function(resolve) {
  40. var will = mdd.then(function(lookup) {
  41. console.log('lookup: ' + id);
  42. return lookup(id);
  43. }).then(load)
  44. .then(function(url) { resolve(url); });
  45. });
  46. }
  47. return entry;
  48. }
  49. return {get: get};
  50. })(resources['mdd']);
  51. function loadData(mime, data) {
  52. var blob = new Blob([data], {type: mime});
  53. return URL.createObjectURL(blob);
  54. }
  55. function loadAudio(ext, data) {
  56. if (ext === 'spx') {
  57. var blob = decodeSpeex(String.fromCharCode.apply(null, data));
  58. return URL.createObjectURL(blob);
  59. } else { // 'spx'
  60. return loadData(MIME[ext] || 'audio', data);
  61. }
  62. }
  63. // TODO: LRU cache: remove oldest one only after rendering.
  64. function replaceImage(index, img) {
  65. var $img = $(img);
  66. var src = $img.attr('src'), m = /^file:\/\/(.*)/.exec(src);
  67. if (m) { src = m[1]; }
  68. cache.get(src, loadData.bind(null, MIME['img']))
  69. .then(function(url) {
  70. $img.attr({src: url, src_: src});
  71. });
  72. }
  73. function playAudio(e, $a) {
  74. ($a || $(this)).find('audio')[0].play();
  75. }
  76. function renderAudio() {
  77. var $a = $(this);
  78. if ($a.attr('href_')) {
  79. playAudio($a);
  80. } else {
  81. var href = $a.attr('href'), res = href.substring(8);
  82. var ext = getExtension(res, 'wav');
  83. cache.get(res, loadAudio.bind(null, ext))
  84. .then(function(url) {
  85. $a.append($('<audio>').attr({src: url, src_: href})).on('click', playAudio);
  86. setTimeout(playAudio.bind($a));
  87. });
  88. }
  89. return false;
  90. }
  91. function replaceCss(index, link) {
  92. var $link = $(link);
  93. var href = $link.attr('href');
  94. cache.get(href, loadData.bind(null, MIME['css']))
  95. .then(function(url) {
  96. // $link.attr({href: url, href_: href});
  97. // TODO: Limit scope of embedded styles provide by mdd file
  98. // TODO: use shadow dom for Chrome
  99. // TODO: use scoped style for Firefox
  100. $link.replaceWith($('<style scoped>', {src_: href}).text('@import url("' + url + '")'));
  101. });
  102. }
  103. function injectJS(index, el) {
  104. var $el = $(el);
  105. var src = $el.attr('src');
  106. cache.get(src, loadData.bind(null, MIME['js']))
  107. .then(function(url) {
  108. $el.remove();
  109. $.ajax({url: url, dataType: 'script', cache: true});
  110. });
  111. }
  112. function decodeSpeex(file) {
  113. var ogg = new Ogg(file, {file: true});
  114. ogg.demux();
  115. var header = Speex.parseHeader(ogg.frames[0]);
  116. console.log(header);
  117. var comment = new SpeexComment(ogg.frames[1]);
  118. console.log(comment.data);
  119. var spx = new Speex({
  120. quality: 8,
  121. mode: header.mode,
  122. rate: header.rate
  123. });
  124. var waveData = PCMData.encode({
  125. sampleRate: header.rate,
  126. channelCount: header.nb_channels,
  127. bytesPerSample: 2,
  128. data: spx.decode(ogg.bitstream(), ogg.segments)
  129. });
  130. return new Blob([Speex.util.str2ab(waveData)], {type: "audio/wav"});
  131. }
  132. function render($content) {
  133. if (resources['mdd']) {
  134. $content.find('img[src]').each(replaceImage);
  135. $content.find('link[rel=stylesheet]').each(replaceCss);
  136. $content.find('script[src]').each(injectJS);
  137. $content.find('a[href^="sound://"]').on('click', renderAudio);
  138. setTimeout(function() { $('#definition *').trigger('resize'); });
  139. }
  140. // resolve entry:// link dynamically in mdict.js
  141. // // rewrite in-page link
  142. // $content.find('a[href^="entry://"]').each(function() {
  143. // var $el = $(this), href = $el.attr('href');
  144. // if (href.match('#')) {
  145. // $el.attr('href', href.substring(8));
  146. // }
  147. // });
  148. return $content;
  149. }
  150. return {
  151. lookup: function lookup(query) {
  152. return (resources['mdx'] || resources['mdd'])
  153. .then(function (lookup) {
  154. return lookup(query);
  155. }).then(function (definitions) {
  156. console.log('lookup done!');
  157. var html = definitions.reduce(function(prev, txt) {
  158. return prev + '<p></p>' + txt;
  159. }, '<p>' + definitions.length + ' entry(ies) </p>');
  160. return Promise.resolve(render($('<div>').html(html)));
  161. });
  162. },
  163. search: function (query) {
  164. return resources['mdx'].then(function(lookup) {
  165. return lookup(query);
  166. });
  167. },
  168. render: render,
  169. };
  170. }
  171. }());