nodes.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. import { nodes } from 'prosemirror-schema-basic'
  2. import type { Node, NodeSpec } from 'prosemirror-model'
  3. import { listItem as _listItem } from 'prosemirror-schema-list'
  4. interface Attr {
  5. [key: string]: number | string
  6. }
  7. const orderedList: NodeSpec = {
  8. attrs: {
  9. order: {
  10. default: 1,
  11. },
  12. listStyleType: {
  13. default: '',
  14. },
  15. fontsize: {
  16. default: '',
  17. },
  18. color: {
  19. default: '',
  20. },
  21. },
  22. content: 'list_item+',
  23. group: 'block',
  24. parseDOM: [
  25. {
  26. tag: 'ol',
  27. getAttrs: dom => {
  28. const order = ((dom as HTMLElement).hasAttribute('start') ? (dom as HTMLElement).getAttribute('start') : 1) || 1
  29. const attr: Attr = { order: +order }
  30. const { listStyleType, fontSize, color } = (dom as HTMLElement).style
  31. if (listStyleType) attr['listStyleType'] = listStyleType
  32. if (fontSize) attr['fontsize'] = fontSize
  33. if (color) attr['color'] = color
  34. return attr
  35. }
  36. }
  37. ],
  38. toDOM: (node: Node) => {
  39. const { order, listStyleType, fontsize, color } = node.attrs
  40. let style = ''
  41. if (listStyleType) style += `list-style-type: ${listStyleType};`
  42. if (fontsize) style += `font-size: ${fontsize};`
  43. if (color) style += `color: ${color};`
  44. const attr: Attr = { style }
  45. if (order !== 1) attr['start'] = order
  46. return ['ol', attr, 0]
  47. },
  48. }
  49. const bulletList: NodeSpec = {
  50. attrs: {
  51. listStyleType: {
  52. default: '',
  53. },
  54. fontsize: {
  55. default: '',
  56. },
  57. color: {
  58. default: '',
  59. },
  60. },
  61. content: 'list_item+',
  62. group: 'block',
  63. parseDOM: [
  64. {
  65. tag: 'ul',
  66. getAttrs: dom => {
  67. const attr: Attr = {}
  68. const { listStyleType, fontSize, color } = (dom as HTMLElement).style
  69. if (listStyleType) attr['listStyleType'] = listStyleType
  70. if (fontSize) attr['fontsize'] = fontSize
  71. if (color) attr['color'] = color
  72. return attr
  73. }
  74. }
  75. ],
  76. toDOM: (node: Node) => {
  77. const { listStyleType, fontsize, color } = node.attrs
  78. let style = ''
  79. if (listStyleType) style += `list-style-type: ${listStyleType};`
  80. if (fontsize) style += `font-size: ${fontsize};`
  81. if (color) style += `color: ${color};`
  82. return ['ul', { style }, 0]
  83. },
  84. }
  85. /*
  86. const listItem: NodeSpec = {
  87. ..._listItem,
  88. content: 'paragraph block*',
  89. group: 'block',
  90. }
  91. */
  92. const listItem: NodeSpec = {
  93. attrs: {
  94. textAlign: { default: '' },
  95. textAlignLast: { default: '' },
  96. textIndent: { default: '' },
  97. marginTop: { default: '' },
  98. marginBottom: { default: '' },
  99. marginLeft: { default: '' },
  100. marginRight: { default: '' },
  101. lineHeight: { default: '' },
  102. paddingTop: { default: '' },
  103. paddingRight: { default: '' },
  104. paddingBottom: { default: '' },
  105. paddingLeft: { default: '' },
  106. whiteSpace: { default: 'normal' }, // 新增 white-space 属性
  107. },
  108. content: 'paragraph block*',
  109. group: 'block',
  110. parseDOM: [
  111. {
  112. tag: 'li',
  113. getAttrs(dom) {
  114. const el = dom as HTMLElement;
  115. const style = el.style;
  116. const textAlign = style.textAlign || '';
  117. const textAlignLast = style.textAlignLast || '';
  118. const textIndent = style.textIndent || '';
  119. const marginTop = style.marginTop || '';
  120. const marginBottom = style.marginBottom || '';
  121. const marginLeft = style.marginLeft || '';
  122. const marginRight = style.marginRight || '';
  123. const lineHeight = style.lineHeight || '';
  124. const paddingTop = style.paddingTop || '';
  125. const paddingRight = style.paddingRight || '';
  126. const paddingBottom = style.paddingBottom || '';
  127. const paddingLeft = style.paddingLeft || '';
  128. const whiteSpace = style.whiteSpace || 'normal'; // 读取 white-space
  129. return {
  130. textAlign,
  131. textAlignLast,
  132. textIndent,
  133. marginTop,
  134. marginBottom,
  135. marginLeft,
  136. marginRight,
  137. lineHeight,
  138. paddingTop,
  139. paddingRight,
  140. paddingBottom,
  141. paddingLeft,
  142. whiteSpace, // 返回 whiteSpace
  143. };
  144. },
  145. },
  146. ],
  147. toDOM(node) {
  148. const {
  149. textAlign,
  150. textAlignLast,
  151. textIndent,
  152. marginTop,
  153. marginBottom,
  154. marginLeft,
  155. marginRight,
  156. lineHeight,
  157. paddingTop,
  158. paddingRight,
  159. paddingBottom,
  160. paddingLeft,
  161. whiteSpace, // 获取 whiteSpace
  162. } = node.attrs;
  163. let style = '';
  164. if (textAlign && textAlign !== 'left') {
  165. style += `text-align: ${textAlign};`;
  166. }
  167. if (textAlignLast) {
  168. style += `text-align-last: ${textAlignLast};`;
  169. }
  170. if (textIndent) {
  171. style += `text-indent: (100% - ${textIndent});`;
  172. }
  173. if (marginTop) style += `margin-top: ${marginTop};`;
  174. if (marginBottom) style += `margin-bottom: ${marginBottom};`;
  175. if (marginLeft) {
  176. // 解析数值和单位
  177. const str = String(marginLeft).trim();
  178. const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
  179. if (match) {
  180. let num = parseFloat(match[1]);
  181. const unit = match[2] || 'px';
  182. const absNum = Math.abs(num); // 负数转正
  183. const val = absNum + unit;
  184. style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
  185. } else {
  186. style += `margin-left: ${marginLeft};`;
  187. }
  188. }
  189. if (marginRight) style += `margin-right: ${marginRight};`;
  190. if (lineHeight) {
  191. let finalValue;
  192. const str = String(lineHeight).trim();
  193. // 匹配纯数字(整数或小数,可带负号)
  194. if (/^-?\d+(\.\d+)?$/.test(str)) {
  195. finalValue = parseFloat(str) * 1.2;
  196. } else {
  197. // 带单位或其他非纯数字内容,直接使用原值
  198. finalValue = lineHeight;
  199. }
  200. style += `line-height: ${finalValue};`;
  201. }
  202. if (paddingTop) style += `padding-top: ${paddingTop};`;
  203. if (paddingRight) style += `padding-right: ${paddingRight};`;
  204. if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
  205. if (paddingLeft) style += `padding-left: ${paddingLeft};`;
  206. if (whiteSpace && whiteSpace !== 'normal') {
  207. style += `white-space: ${whiteSpace};`; // 添加 white-space
  208. }
  209. //const attrs: { style?: string } = {};
  210. //if (style) attrs.style = style;
  211. let isEmpty = false;
  212. const firstChild = node.content.firstChild;
  213. if (firstChild && firstChild.type.name === 'paragraph') {
  214. // 段落无任何子节点(包括 text 和 inline 节点)
  215. if (firstChild.content.size === 0) {
  216. isEmpty = true;
  217. }
  218. }
  219. // 如果整个 li 的内容长度为 0 也可以作为判断
  220. if (node.content.size === 0) isEmpty = true;
  221. const attrs: { style?: string; class?: string } = {};
  222. if (style) attrs.style = style;
  223. if (isEmpty) attrs.class = 'empty-li';
  224. return ['li', attrs, 0];
  225. },
  226. };
  227. const paragraph: NodeSpec = {
  228. whitespace: "pre", // 此属性控制 ProseMirror 内部空格处理,与 CSS white-space 无关,保留不变
  229. attrs: {
  230. textAlign: { default: '' },
  231. textAlignLast: { default: '' },
  232. indent: { default: 0 },
  233. textIndent: { default: 0 },
  234. marginTop: { default: '' },
  235. marginBottom: { default: '' },
  236. marginLeft: { default: '' },
  237. marginRight: { default: '' },
  238. lineHeight: { default: '' },
  239. paddingTop: { default: '' },
  240. paddingRight: { default: '' },
  241. paddingBottom: { default: '' },
  242. paddingLeft: { default: '' },
  243. whiteSpace: { default: 'normal' }, // 新增 white-space 属性
  244. },
  245. content: 'inline*',
  246. group: 'block',
  247. parseDOM: [
  248. {
  249. tag: 'p',
  250. getAttrs: dom => {
  251. const el = dom as HTMLElement;
  252. const style = el.style;
  253. const textAlign = style.textAlign || '';
  254. const textAlignLast = style.textAlignLast || '';
  255. const marginTop = style.marginTop || '';
  256. const marginBottom = style.marginBottom || '';
  257. const marginLeft = style.marginLeft || '';
  258. const marginRight = style.marginRight || '';
  259. const textIndent = style.textIndent || '';
  260. const lineHeight = style.lineHeight || '';
  261. const paddingTop = style.paddingTop || '';
  262. const paddingRight = style.paddingRight || '';
  263. const paddingBottom = style.paddingBottom || '';
  264. const paddingLeft = style.paddingLeft || '';
  265. const whiteSpace = style.whiteSpace || 'normal'; // 读取 white-space
  266. return {
  267. textAlign,
  268. textAlignLast,
  269. textIndent,
  270. marginTop,
  271. marginBottom,
  272. marginLeft,
  273. marginRight,
  274. lineHeight,
  275. paddingTop,
  276. paddingRight,
  277. paddingBottom,
  278. paddingLeft,
  279. whiteSpace, // 返回 whiteSpace
  280. };
  281. },
  282. },
  283. {
  284. tag: 'img',
  285. ignore: true,
  286. },
  287. {
  288. tag: 'pre',
  289. skip: true,
  290. },
  291. ],
  292. toDOM: (node: Node) => {
  293. const {
  294. textAlign,
  295. textAlignLast,
  296. textIndent,
  297. marginTop,
  298. marginBottom,
  299. marginLeft,
  300. marginRight,
  301. lineHeight,
  302. paddingTop,
  303. paddingRight,
  304. paddingBottom,
  305. paddingLeft,
  306. whiteSpace, // 获取 whiteSpace
  307. } = node.attrs;
  308. let style = '';
  309. if (textAlign && textAlign !== 'left') {
  310. style += `text-align: ${textAlign};`;
  311. }
  312. if (textAlignLast) {
  313. style += `text-align-last: ${textAlignLast};`;
  314. }
  315. if (textIndent) {
  316. style += `text-indent: (100% - ${textIndent});`;
  317. }
  318. if (marginTop) style += `margin-top: ${marginTop};`;
  319. if (marginBottom) style += `margin-bottom: ${marginBottom};`;
  320. /*
  321. if (marginLeft) {
  322. // 解析数值和单位
  323. const str = String(marginLeft).trim();
  324. const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
  325. if (match) {
  326. let num = parseFloat(match[1]);
  327. const unit = match[2] || 'px';
  328. const absNum = Math.abs(num); // 负数转正
  329. const val = absNum + unit;
  330. style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
  331. } else {
  332. style += `margin-left: ${marginLeft};`;
  333. }
  334. }
  335. */
  336. if (marginLeft) style += `margin-left: ${marginLeft};`;
  337. if (marginRight) style += `margin-right: ${marginRight};`;
  338. if (lineHeight) {
  339. let finalValue;
  340. const str = String(lineHeight).trim();
  341. // 匹配纯数字(整数或小数,可带负号)
  342. if (/^-?\d+(\.\d+)?$/.test(str)) {
  343. finalValue = parseFloat(str) * 1.2;
  344. } else {
  345. // 带单位或其他非纯数字内容,直接使用原值
  346. finalValue = lineHeight;
  347. }
  348. style += `line-height: ${finalValue};`;
  349. }
  350. if (paddingTop) style += `padding-top: ${paddingTop};`;
  351. if (paddingRight) style += `padding-right: ${paddingRight};`;
  352. if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
  353. if (paddingLeft) style += `padding-left: ${paddingLeft};`;
  354. if (whiteSpace && whiteSpace !== 'normal') {
  355. style += `white-space: ${whiteSpace};`; // 添加 white-space
  356. }
  357. const attrs: { style?: string; class?: string } = {};
  358. /*
  359. let isEmpty = false;
  360. const firstChild = node.content.firstChild;
  361. if (firstChild) {
  362. // 段落无任何子节点(包括 text 和 inline 节点)
  363. if (firstChild.content.size === 0) {
  364. isEmpty = true;
  365. }
  366. }
  367. // 如果整个 li 的内容长度为 0 也可以作为判断
  368. if (node.content.size === 0) isEmpty = true;
  369. if (style) attrs.style = style;
  370. //if (isEmpty) attrs.class = 'empty';
  371. */
  372. if (style) attrs.style = style;
  373. return ['p', attrs, 0];
  374. },
  375. };
  376. const hardBreak: NodeSpec = {
  377. inline: true, // 内联节点
  378. group: 'inline', // 属于 inline 组
  379. selectable: false, // 不可被光标单独选中
  380. parseDOM: [{ tag: 'br' }],
  381. toDOM() {
  382. return ['br'];
  383. },
  384. };
  385. const {
  386. doc,
  387. blockquote,
  388. text,
  389. } = nodes
  390. export default {
  391. doc,
  392. paragraph,
  393. blockquote,
  394. text,
  395. 'ordered_list': orderedList,
  396. 'bullet_list': bulletList,
  397. 'list_item': listItem,
  398. hard_break: hardBreak, // 新增
  399. }