widget.ts 8.8 KB

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