widget.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { showErrorMessage } from '@jupyterlab/apputils';
  4. import { ActivityMonitor } from '@jupyterlab/coreutils';
  5. import {
  6. ABCWidgetFactory,
  7. DocumentRegistry,
  8. DocumentWidget
  9. } from '@jupyterlab/docregistry';
  10. import {
  11. IRenderMime,
  12. RenderMimeRegistry,
  13. MimeModel
  14. } from '@jupyterlab/rendermime';
  15. import { PromiseDelegate } from '@phosphor/coreutils';
  16. import { Message } from '@phosphor/messaging';
  17. import { JSONObject } from '@phosphor/coreutils';
  18. import { StackedLayout, Widget } from '@phosphor/widgets';
  19. /**
  20. * The class name added to a markdown viewer.
  21. */
  22. const MARKDOWNVIEWER_CLASS = 'jp-MarkdownViewer';
  23. /**
  24. * The markdown MIME type.
  25. */
  26. const MIMETYPE = 'text/markdown';
  27. /**
  28. * A widget for markdown documents.
  29. */
  30. export class MarkdownViewer extends Widget {
  31. /**
  32. * Construct a new markdown viewer widget.
  33. */
  34. constructor(options: MarkdownViewer.IOptions) {
  35. super();
  36. this.context = options.context;
  37. this.renderer = options.renderer;
  38. this.node.tabIndex = -1;
  39. this.addClass(MARKDOWNVIEWER_CLASS);
  40. const layout = (this.layout = new StackedLayout());
  41. layout.addWidget(this.renderer);
  42. this.context.ready.then(async () => {
  43. await this._render();
  44. // Throttle the rendering rate of the widget.
  45. this._monitor = new ActivityMonitor({
  46. signal: this.context.model.contentChanged,
  47. timeout: this._config.renderTimeout
  48. });
  49. this._monitor.activityStopped.connect(
  50. this.update,
  51. this
  52. );
  53. this._ready.resolve(undefined);
  54. });
  55. }
  56. /**
  57. * A promise that resolves when the markdown viewer is ready.
  58. */
  59. get ready(): Promise<void> {
  60. return this._ready.promise;
  61. }
  62. /**
  63. * Set URI fragment identifier.
  64. */
  65. setFragment(fragment: string) {
  66. this._fragment = fragment;
  67. this.update();
  68. }
  69. /**
  70. * Set a config option for the markdown viewer.
  71. */
  72. setOption<K extends keyof MarkdownViewer.IConfig>(
  73. option: K,
  74. value: MarkdownViewer.IConfig[K]
  75. ): void {
  76. if (this._config[option] === value) {
  77. return;
  78. }
  79. this._config[option] = value;
  80. const { style } = this.renderer.node;
  81. switch (option) {
  82. case 'fontFamily':
  83. style.fontFamily = value as string | null;
  84. break;
  85. case 'fontSize':
  86. style.fontSize = value ? value + 'px' : null;
  87. break;
  88. case 'hideFrontMatter':
  89. this.update();
  90. break;
  91. case 'lineHeight':
  92. style.lineHeight = value ? value.toString() : null;
  93. break;
  94. case 'lineWidth':
  95. const padding = value ? `calc(50% - ${(value as number) / 2}ch)` : null;
  96. style.paddingLeft = padding;
  97. style.paddingRight = padding;
  98. break;
  99. case 'renderTimeout':
  100. this._monitor.timeout = value as number;
  101. break;
  102. default:
  103. break;
  104. }
  105. }
  106. /**
  107. * Dispose of the resources held by the widget.
  108. */
  109. dispose(): void {
  110. if (this.isDisposed) {
  111. return;
  112. }
  113. if (this._monitor) {
  114. this._monitor.dispose();
  115. }
  116. this._monitor = null;
  117. super.dispose();
  118. }
  119. /**
  120. * Handle an `update-request` message to the widget.
  121. */
  122. protected onUpdateRequest(msg: Message): void {
  123. if (this.context.isReady && !this.isDisposed) {
  124. this._render();
  125. this._fragment = '';
  126. }
  127. }
  128. /**
  129. * Handle `'activate-request'` messages.
  130. */
  131. protected onActivateRequest(msg: Message): void {
  132. this.node.focus();
  133. }
  134. /**
  135. * Render the mime content.
  136. */
  137. private async _render(): Promise<void> {
  138. if (this.isDisposed) {
  139. return;
  140. }
  141. // Since rendering is async, we note render requests that happen while we
  142. // actually are rendering for a future rendering.
  143. if (this._isRendering) {
  144. this._renderRequested = true;
  145. return;
  146. }
  147. // Set up for this rendering pass.
  148. this._renderRequested = false;
  149. const { context } = this;
  150. const { model } = context;
  151. const source = model.toString();
  152. const data: JSONObject = {};
  153. // If `hideFrontMatter`is true remove front matter.
  154. data[MIMETYPE] = this._config.hideFrontMatter
  155. ? Private.removeFrontMatter(source)
  156. : source;
  157. const mimeModel = new MimeModel({
  158. data,
  159. metadata: { fragment: this._fragment }
  160. });
  161. try {
  162. // Do the rendering asynchronously.
  163. this._isRendering = true;
  164. await this.renderer.renderModel(mimeModel);
  165. this._isRendering = false;
  166. // If there is an outstanding request to render, go ahead and render
  167. if (this._renderRequested) {
  168. return this._render();
  169. }
  170. } catch (reason) {
  171. // Dispose the document if rendering fails.
  172. requestAnimationFrame(() => {
  173. this.dispose();
  174. });
  175. showErrorMessage(`Renderer Failure: ${context.path}`, reason);
  176. }
  177. }
  178. readonly context: DocumentRegistry.Context;
  179. readonly renderer: IRenderMime.IRenderer;
  180. private _config = { ...MarkdownViewer.defaultConfig };
  181. private _fragment = '';
  182. private _monitor: ActivityMonitor<any, any> | null;
  183. private _ready = new PromiseDelegate<void>();
  184. private _isRendering = false;
  185. private _renderRequested = false;
  186. }
  187. /**
  188. * The namespace for MarkdownViewer class statics.
  189. */
  190. export namespace MarkdownViewer {
  191. /**
  192. * The options used to initialize a MarkdownViewer.
  193. */
  194. export interface IOptions {
  195. /**
  196. * Context
  197. */
  198. context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
  199. /**
  200. * The renderer instance.
  201. */
  202. renderer: IRenderMime.IRenderer;
  203. }
  204. export interface IConfig {
  205. /**
  206. * User preferred font family for markdown viewer.
  207. */
  208. fontFamily: string | null;
  209. /**
  210. * User preferred size in pixel of the font used in markdown viewer.
  211. */
  212. fontSize: number | null;
  213. /**
  214. * User preferred text line height, as a multiplier of font size.
  215. */
  216. lineHeight: number | null;
  217. /**
  218. * User preferred text line width expressed in CSS ch units.
  219. */
  220. lineWidth: number | null;
  221. /**
  222. * Whether to hide the YALM front matter.
  223. */
  224. hideFrontMatter: boolean;
  225. /**
  226. * The render timeout.
  227. */
  228. renderTimeout: number;
  229. }
  230. /**
  231. * The default configuration options for an editor.
  232. */
  233. export const defaultConfig: MarkdownViewer.IConfig = {
  234. fontFamily: null,
  235. fontSize: null,
  236. lineHeight: null,
  237. lineWidth: null,
  238. hideFrontMatter: true,
  239. renderTimeout: 1000
  240. };
  241. }
  242. /**
  243. * A document widget for markdown content.
  244. */
  245. export class MarkdownDocument extends DocumentWidget<MarkdownViewer> {
  246. setFragment(fragment: string): void {
  247. this.content.setFragment(fragment);
  248. }
  249. }
  250. /**
  251. * A widget factory for markdown viewers.
  252. */
  253. export class MarkdownViewerFactory extends ABCWidgetFactory<MarkdownDocument> {
  254. /**
  255. * Construct a new mimetype widget factory.
  256. */
  257. constructor(options: MarkdownViewerFactory.IOptions) {
  258. super(Private.createRegistryOptions(options));
  259. this._fileType = options.primaryFileType;
  260. this._rendermime = options.rendermime;
  261. }
  262. /**
  263. * Create a new widget given a context.
  264. */
  265. protected createNewWidget(
  266. context: DocumentRegistry.Context
  267. ): MarkdownDocument {
  268. const rendermime = this._rendermime.clone({
  269. resolver: context.urlResolver
  270. });
  271. const renderer = rendermime.createRenderer(MIMETYPE);
  272. const content = new MarkdownViewer({ context, renderer });
  273. content.title.iconClass = this._fileType.iconClass;
  274. content.title.iconLabel = this._fileType.iconLabel;
  275. const widget = new MarkdownDocument({ content, context });
  276. return widget;
  277. }
  278. private _fileType: DocumentRegistry.IFileType;
  279. private _rendermime: RenderMimeRegistry;
  280. }
  281. /**
  282. * The namespace for MimeDocumentFactory class statics.
  283. */
  284. export namespace MarkdownViewerFactory {
  285. /**
  286. * The options used to initialize a MimeDocumentFactory.
  287. */
  288. export interface IOptions extends DocumentRegistry.IWidgetFactoryOptions {
  289. /**
  290. * The primary file type associated with the document.
  291. */
  292. primaryFileType: DocumentRegistry.IFileType;
  293. /**
  294. * The rendermime instance.
  295. */
  296. rendermime: RenderMimeRegistry;
  297. }
  298. }
  299. /**
  300. * A namespace for markdown viewer widget private data.
  301. */
  302. namespace Private {
  303. /**
  304. * Create the document registry options.
  305. */
  306. export function createRegistryOptions(
  307. options: MarkdownViewerFactory.IOptions
  308. ): DocumentRegistry.IWidgetFactoryOptions {
  309. return {
  310. ...options,
  311. readOnly: true
  312. } as DocumentRegistry.IWidgetFactoryOptions;
  313. }
  314. /**
  315. * Remove YALM front matter from source.
  316. */
  317. export function removeFrontMatter(source: string): string {
  318. const re = /^---\n[^]*?\n(---|...)\n/;
  319. const match = source.match(re);
  320. if (!match) {
  321. return source;
  322. }
  323. const { length } = match[0];
  324. return source.slice(length);
  325. }
  326. }