shared.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Cell } from '@jupyterlab/cells';
  4. import { IHeading } from '../toc';
  5. const VDOM_MIME_TYPE = 'application/vdom.v1+json';
  6. const HTML_MIME_TYPE = 'text/html';
  7. export enum INotebookHeadingTypes {
  8. header,
  9. markdown,
  10. code
  11. }
  12. export interface INotebookHeading extends INumberedHeading {
  13. type: INotebookHeadingTypes;
  14. prompt?: string;
  15. cellRef?: Cell;
  16. hasChild?: boolean;
  17. }
  18. export interface INumberedHeading extends IHeading {
  19. numbering?: string | null;
  20. }
  21. /**
  22. * Given a dictionary that keep tracks of the numbering and the level,
  23. * update the dictionary.
  24. */
  25. function incrementNumberingDict(dict: any, level: number) {
  26. if (dict[level + 1] != undefined) {
  27. dict[level + 1] = undefined;
  28. }
  29. if (dict[level] === undefined) {
  30. dict[level] = 1;
  31. } else {
  32. dict[level]++;
  33. }
  34. }
  35. /**
  36. * Given a dictionary that keep tracks of the numbering and the current level,
  37. * generate the current numbering based on the dictionary and current level.
  38. */
  39. export function generateNumbering(
  40. numberingDict: { [level: number]: number },
  41. level: number
  42. ) {
  43. let numbering = undefined;
  44. if (numberingDict != null) {
  45. incrementNumberingDict(numberingDict, level);
  46. numbering = '';
  47. for (let j = 1; j <= level; j++) {
  48. numbering +=
  49. (numberingDict[j] == undefined ? '0' : numberingDict[j]) + '.';
  50. if (j === level) {
  51. numbering += ' ';
  52. }
  53. }
  54. }
  55. return numbering;
  56. }
  57. /**
  58. * Given a string of markdown, get the markdown headings
  59. * in that string.
  60. */
  61. export function getMarkdownHeadings(
  62. text: string,
  63. onClickFactory: (line: number) => (() => void),
  64. numberingDict: any,
  65. lastLevel: number,
  66. cellRef: Cell
  67. ): INotebookHeading[] {
  68. // Split the text into lines.
  69. const lines = text.split('\n');
  70. let headings: INotebookHeading[] = [];
  71. // Iterate over the lines to get the header level and
  72. // the text for the line.
  73. let line = lines[0];
  74. let idx = 0;
  75. // Make an onClick handler for this line.
  76. const onClick = onClickFactory(idx);
  77. // First test for '#'-style headers.
  78. let match = line.match(/^([#]{1,6}) (.*)/);
  79. let match2 = line.match(/^([=]{2,}|[-]{2,})/);
  80. let match3 = line.match(/<h([1-6])>(.*)<\/h\1>/i);
  81. if (match) {
  82. const level = match[1].length;
  83. // Take special care to parse markdown links into raw text.
  84. const text = match[2].replace(/\[(.+)\]\(.+\)/g, '$1');
  85. let numbering = generateNumbering(numberingDict, level);
  86. headings.push({
  87. text,
  88. level,
  89. numbering,
  90. onClick,
  91. type: INotebookHeadingTypes.header,
  92. cellRef: cellRef,
  93. hasChild: true
  94. });
  95. } else if (match2 && idx > 0) {
  96. // Next test for '==='-style headers.
  97. const level = match2[1][0] === '=' ? 1 : 2;
  98. // Take special care to parse markdown links into raw text.
  99. const text = lines[idx - 1].replace(/\[(.+)\]\(.+\)/g, '$1');
  100. let numbering = generateNumbering(numberingDict, level);
  101. headings.push({
  102. text,
  103. level,
  104. numbering,
  105. onClick,
  106. type: INotebookHeadingTypes.header,
  107. cellRef: cellRef,
  108. hasChild: true
  109. });
  110. } else if (match3) {
  111. // Finally test for HTML headers. This will not catch multiline
  112. // headers, nor will it catch multiple headers on the same line.
  113. // It should do a decent job of catching many, though.
  114. const level = parseInt(match3[1], 10);
  115. const text = match3[2];
  116. let numbering = generateNumbering(numberingDict, level);
  117. headings.push({
  118. text,
  119. level,
  120. numbering,
  121. onClick,
  122. type: INotebookHeadingTypes.header,
  123. cellRef: cellRef,
  124. hasChild: true
  125. });
  126. } else {
  127. headings.push({
  128. text: line,
  129. level: lastLevel + 1,
  130. onClick,
  131. type: INotebookHeadingTypes.markdown,
  132. cellRef: cellRef,
  133. hasChild: false
  134. });
  135. }
  136. return headings;
  137. }
  138. /**
  139. * Return whether the mime type is some flavor of markdown.
  140. */
  141. export function isMarkdown(mime: string): boolean {
  142. return (
  143. mime === 'text/x-ipythongfm' ||
  144. mime === 'text/x-markdown' ||
  145. mime === 'text/x-gfm' ||
  146. mime === 'text/markdown'
  147. );
  148. }
  149. /**
  150. * Return whether the mime type is DOM-ish (html or vdom).
  151. */
  152. export function isDOM(mime: string): boolean {
  153. return mime === VDOM_MIME_TYPE || mime === HTML_MIME_TYPE;
  154. }
  155. /**
  156. * Allowed HTML tags for the ToC entries. We use this to
  157. * sanitize HTML headings, if they are given. We specifically
  158. * disallow anchor tags, since we are adding our own.
  159. */
  160. export const sanitizerOptions = {
  161. allowedTags: [
  162. 'p',
  163. 'blockquote',
  164. 'b',
  165. 'i',
  166. 'strong',
  167. 'em',
  168. 'strike',
  169. 'code',
  170. 'br',
  171. 'div',
  172. 'span',
  173. 'pre',
  174. 'del'
  175. ],
  176. allowedAttributes: {
  177. // Allow "class" attribute for <code> tags.
  178. code: ['class'],
  179. // Allow "class" attribute for <span> tags.
  180. span: ['class'],
  181. // Allow "class" attribute for <div> tags.
  182. div: ['class'],
  183. // Allow "class" attribute for <p> tags.
  184. p: ['class'],
  185. // Allow "class" attribute for <pre> tags.
  186. pre: ['class']
  187. }
  188. };