@aquera/nile-elements 1.8.5 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +8 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +915 -321
  5. package/dist/nile-markdown/index.cjs.js +2 -0
  6. package/dist/nile-markdown/index.cjs.js.map +1 -0
  7. package/dist/nile-markdown/index.esm.js +1 -0
  8. package/dist/nile-markdown/nile-markdown.cjs.js +30 -0
  9. package/dist/nile-markdown/nile-markdown.cjs.js.map +1 -0
  10. package/dist/nile-markdown/nile-markdown.css.cjs.js +2 -0
  11. package/dist/nile-markdown/nile-markdown.css.cjs.js.map +1 -0
  12. package/dist/nile-markdown/nile-markdown.css.esm.js +152 -0
  13. package/dist/nile-markdown/nile-markdown.esm.js +3 -0
  14. package/dist/nile-markdown-editor/index.cjs.js +2 -0
  15. package/dist/nile-markdown-editor/index.cjs.js.map +1 -0
  16. package/dist/nile-markdown-editor/index.esm.js +1 -0
  17. package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js +2 -0
  18. package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js.map +1 -0
  19. package/dist/nile-markdown-editor/nile-markdown-editor.css.cjs.js +2 -0
  20. package/dist/nile-markdown-editor/nile-markdown-editor.css.cjs.js.map +1 -0
  21. package/dist/nile-markdown-editor/nile-markdown-editor.css.esm.js +255 -0
  22. package/dist/nile-markdown-editor/nile-markdown-editor.esm.js +143 -0
  23. package/dist/nile-option/nile-option.cjs.js +1 -1
  24. package/dist/nile-option/nile-option.cjs.js.map +1 -1
  25. package/dist/nile-option/nile-option.css.cjs.js +1 -1
  26. package/dist/nile-option/nile-option.css.cjs.js.map +1 -1
  27. package/dist/nile-option/nile-option.css.esm.js +22 -1
  28. package/dist/nile-option/nile-option.esm.js +12 -2
  29. package/dist/nile-select/nile-select.cjs.js +1 -1
  30. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  31. package/dist/nile-select/nile-select.css.cjs.js +1 -1
  32. package/dist/nile-select/nile-select.css.cjs.js.map +1 -1
  33. package/dist/nile-select/nile-select.css.esm.js +16 -7
  34. package/dist/nile-select/nile-select.esm.js +2 -2
  35. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  36. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  37. package/dist/nile-select/virtual-scroll-helper.esm.js +2 -0
  38. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +3 -3
  39. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  40. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js +1 -1
  41. package/dist/nile-virtual-select/nile-virtual-select.css.cjs.js.map +1 -1
  42. package/dist/nile-virtual-select/nile-virtual-select.css.esm.js +4 -3
  43. package/dist/nile-virtual-select/nile-virtual-select.esm.js +4 -4
  44. package/dist/nile-virtual-select/renderer.cjs.js +1 -1
  45. package/dist/nile-virtual-select/renderer.cjs.js.map +1 -1
  46. package/dist/nile-virtual-select/renderer.esm.js +14 -12
  47. package/dist/src/index.d.ts +2 -0
  48. package/dist/src/index.js +2 -0
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/nile-markdown/index.d.ts +1 -0
  51. package/dist/src/nile-markdown/index.js +2 -0
  52. package/dist/src/nile-markdown/index.js.map +1 -0
  53. package/dist/src/nile-markdown/nile-markdown.css.d.ts +10 -0
  54. package/dist/src/nile-markdown/nile-markdown.css.js +163 -0
  55. package/dist/src/nile-markdown/nile-markdown.css.js.map +1 -0
  56. package/dist/src/nile-markdown/nile-markdown.d.ts +91 -0
  57. package/dist/src/nile-markdown/nile-markdown.js +167 -0
  58. package/dist/src/nile-markdown/nile-markdown.js.map +1 -0
  59. package/dist/src/nile-markdown/nile-markdown.test.d.ts +1 -0
  60. package/dist/src/nile-markdown/nile-markdown.test.js +192 -0
  61. package/dist/src/nile-markdown/nile-markdown.test.js.map +1 -0
  62. package/dist/src/nile-markdown-editor/index.d.ts +1 -0
  63. package/dist/src/nile-markdown-editor/index.js +2 -0
  64. package/dist/src/nile-markdown-editor/index.js.map +1 -0
  65. package/dist/src/nile-markdown-editor/nile-markdown-editor.css.d.ts +10 -0
  66. package/dist/src/nile-markdown-editor/nile-markdown-editor.css.js +266 -0
  67. package/dist/src/nile-markdown-editor/nile-markdown-editor.css.js.map +1 -0
  68. package/dist/src/nile-markdown-editor/nile-markdown-editor.d.ts +121 -0
  69. package/dist/src/nile-markdown-editor/nile-markdown-editor.js +615 -0
  70. package/dist/src/nile-markdown-editor/nile-markdown-editor.js.map +1 -0
  71. package/dist/src/nile-markdown-editor/nile-markdown-editor.test.d.ts +1 -0
  72. package/dist/src/nile-markdown-editor/nile-markdown-editor.test.js +268 -0
  73. package/dist/src/nile-markdown-editor/nile-markdown-editor.test.js.map +1 -0
  74. package/dist/src/nile-option/nile-option.css.js +22 -1
  75. package/dist/src/nile-option/nile-option.css.js.map +1 -1
  76. package/dist/src/nile-option/nile-option.d.ts +3 -0
  77. package/dist/src/nile-option/nile-option.js +21 -0
  78. package/dist/src/nile-option/nile-option.js.map +1 -1
  79. package/dist/src/nile-select/nile-select.css.js +16 -7
  80. package/dist/src/nile-select/nile-select.css.js.map +1 -1
  81. package/dist/src/nile-select/nile-select.d.ts +7 -0
  82. package/dist/src/nile-select/nile-select.js +35 -0
  83. package/dist/src/nile-select/nile-select.js.map +1 -1
  84. package/dist/src/nile-select/virtual-scroll-helper.js +2 -0
  85. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  86. package/dist/src/nile-virtual-select/nile-virtual-select.css.js +4 -3
  87. package/dist/src/nile-virtual-select/nile-virtual-select.css.js.map +1 -1
  88. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +4 -0
  89. package/dist/src/nile-virtual-select/nile-virtual-select.js +11 -1
  90. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  91. package/dist/src/nile-virtual-select/renderer.d.ts +2 -2
  92. package/dist/src/nile-virtual-select/renderer.js +6 -4
  93. package/dist/src/nile-virtual-select/renderer.js.map +1 -1
  94. package/dist/src/version.js +1 -1
  95. package/dist/src/version.js.map +1 -1
  96. package/dist/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +2 -1
  98. package/src/index.ts +3 -1
  99. package/src/nile-markdown/index.ts +1 -0
  100. package/src/nile-markdown/nile-markdown.css.ts +164 -0
  101. package/src/nile-markdown/nile-markdown.test.ts +252 -0
  102. package/src/nile-markdown/nile-markdown.ts +179 -0
  103. package/src/nile-markdown-editor/index.ts +1 -0
  104. package/src/nile-markdown-editor/nile-markdown-editor.css.ts +267 -0
  105. package/src/nile-markdown-editor/nile-markdown-editor.test.ts +402 -0
  106. package/src/nile-markdown-editor/nile-markdown-editor.ts +710 -0
  107. package/src/nile-option/nile-option.css.ts +22 -1
  108. package/src/nile-option/nile-option.ts +18 -0
  109. package/src/nile-select/nile-select.css.ts +16 -7
  110. package/src/nile-select/nile-select.ts +32 -0
  111. package/src/nile-select/virtual-scroll-helper.ts +2 -0
  112. package/src/nile-virtual-select/nile-virtual-select.css.ts +4 -3
  113. package/src/nile-virtual-select/nile-virtual-select.ts +9 -1
  114. package/src/nile-virtual-select/renderer.ts +9 -3
  115. package/vscode-html-custom-data.json +115 -3
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent nile-elements following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "nile-elements",
6
- "version": "1.8.5",
6
+ "version": "1.8.7",
7
7
  "main": "dist/src/index.js",
8
8
  "type": "module",
9
9
  "module": "dist/src/index.js",
@@ -167,6 +167,7 @@
167
167
  "element-internals-polyfill": "^1.1.20",
168
168
  "figlet": "1.7.0",
169
169
  "lit": "^3.0.0",
170
+ "marked": "^15.0.12",
170
171
  "tippy.js": "^6.3.7"
171
172
  },
172
173
  "devDependencies": {
package/src/index.ts CHANGED
@@ -131,4 +131,6 @@ export { NileContextMenu } from './nile-context-menu';
131
131
  export { NileContextMenuGroup } from './nile-context-menu-group';
132
132
  export { NileContextMenuItem } from './nile-context-menu-item';
133
133
  export { NileInlineSidebarItemHeader } from './nile-inline-sidebar-item-header';
134
- export { NileInlineSidebarItemBody } from './nile-inline-sidebar-item-body';
134
+ export { NileInlineSidebarItemBody } from './nile-inline-sidebar-item-body';
135
+ export { NileMarkdown } from './nile-markdown';
136
+ export { NileMarkdownEditor } from './nile-markdown-editor';
@@ -0,0 +1 @@
1
+ export { NileMarkdown } from './nile-markdown';
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { css } from 'lit';
9
+
10
+ /**
11
+ * Markdown CSS
12
+ */
13
+ export const styles = css`
14
+ :host {
15
+ display: block;
16
+ -webkit-font-smoothing: var(
17
+ --nile-webkit-font-smoothing,
18
+ var(--ng-webkit-font-smoothing)
19
+ );
20
+ -moz-osx-font-smoothing: var(
21
+ --nile-moz-osx-font-smoothing,
22
+ var(--ng-moz-osx-font-smoothing)
23
+ );
24
+ text-rendering: var(--nile-text-rendering, var(--ng-text-rendering));
25
+ }
26
+
27
+ .markdown {
28
+ font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
29
+ font-size: var(--nile-type-scale-3, var(--ng-font-size-text-md));
30
+ line-height: var(--nile-line-height-medium, var(--ng-line-height-text-md));
31
+ color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
32
+ word-wrap: break-word;
33
+ }
34
+
35
+ .markdown > :first-child {
36
+ margin-top: 0;
37
+ }
38
+
39
+ .markdown > :last-child {
40
+ margin-bottom: 0;
41
+ }
42
+
43
+ .markdown h1,
44
+ .markdown h2,
45
+ .markdown h3,
46
+ .markdown h4,
47
+ .markdown h5,
48
+ .markdown h6 {
49
+ margin: 1.5em 0 0.5em;
50
+ font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-semibold));
51
+ line-height: 1.25;
52
+ color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
53
+ }
54
+
55
+ .markdown h1 {
56
+ font-size: var(--nile-type-scale-7, var(--ng-font-size-display-sm));
57
+ }
58
+
59
+ .markdown h2 {
60
+ font-size: var(--nile-type-scale-6, var(--ng-font-size-text-xl));
61
+ }
62
+
63
+ .markdown h3 {
64
+ font-size: var(--nile-type-scale-5, var(--ng-font-size-text-lg));
65
+ }
66
+
67
+ .markdown h4 {
68
+ font-size: var(--nile-type-scale-3, var(--ng-font-size-text-md));
69
+ }
70
+
71
+ .markdown h5 {
72
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-sm));
73
+ }
74
+
75
+ .markdown h6 {
76
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-sm));
77
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
78
+ }
79
+
80
+ .markdown p {
81
+ margin: 0 0 1em;
82
+ }
83
+
84
+ .markdown a {
85
+ color: var(--nile-colors-link-primary, var(--ng-colors-text-brand-secondary-700)
86
+ );
87
+ text-decoration: underline;
88
+ }
89
+
90
+ .markdown a:hover {
91
+ color: var(--nile-colors-link-hover, var(--ng-colors-text-brand-secondary-hover));
92
+ }
93
+
94
+ .markdown ul,
95
+ .markdown ol {
96
+ margin: 0 0 1em;
97
+ padding-inline-start: 2em;
98
+ }
99
+
100
+ .markdown li > ul,
101
+ .markdown li > ol {
102
+ margin-bottom: 0;
103
+ }
104
+
105
+ .markdown blockquote {
106
+ margin: 0 0 1em;
107
+ padding: 0 1em;
108
+ border-inline-start: 4px solid var(--nile-colors-neutral-400, var(--ng-colors-fg-quaternary-400));
109
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
110
+ }
111
+
112
+ .markdown code {
113
+ font-family: var(--nile-font-family-mono, monospace);
114
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-sm));
115
+ background: var(--nile-colors-dark-100, var(--ng-colors-bg-secondary));
116
+ border-radius: var(--nile-radius-sm, var(--ng-radius-sm));
117
+ padding: 0.125em 0.375em;
118
+ }
119
+
120
+ .markdown pre {
121
+ margin: 0 0 1em;
122
+ padding: var(--nile-spacing-md, var(--ng-spacing-md));
123
+ background: var(--nile-colors-dark-100, var(--ng-colors-bg-secondary));
124
+ border-radius: var(--nile-radius-md, var(--ng-radius-md));
125
+ overflow-x: auto;
126
+ }
127
+
128
+ .markdown pre code {
129
+ background: none;
130
+ border-radius: 0;
131
+ padding: 0;
132
+ }
133
+
134
+ .markdown table {
135
+ margin: 0 0 1em;
136
+ border-collapse: collapse;
137
+ width: max-content;
138
+ max-width: 100%;
139
+ }
140
+
141
+ .markdown th,
142
+ .markdown td {
143
+ border: 1px solid var(--nile-colors-gray-300, var(--ng-colors-gray-300));
144
+ padding: var(--nile-spacing-xs, var(--ng-spacing-xs))
145
+ var(--nile-spacing-md, var(--ng-spacing-md));
146
+ }
147
+
148
+ .markdown th {
149
+ background: var(--nile-colors-dark-100, var(--ng-colors-bg-secondary));
150
+ font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-semibold));
151
+ text-align: left;
152
+ }
153
+
154
+ .markdown img {
155
+ max-width: 100%;
156
+ }
157
+
158
+ .markdown hr {
159
+ height: 1px;
160
+ margin: 1.5em 0;
161
+ border: none;
162
+ background: var(--nile-colors-dark-200, var(--ng-colors-border-secondary));
163
+ }
164
+ `;
@@ -0,0 +1,252 @@
1
+ import { expect, fixture, html, oneEvent } from '@open-wc/testing';
2
+ import './nile-markdown';
3
+ import NileMarkdown from './nile-markdown';
4
+
5
+ describe('NileMarkdown', () => {
6
+ // === RENDERING ===
7
+ it('1. should render without errors', async () => {
8
+ const el = await fixture<NileMarkdown>(
9
+ html`<nile-markdown></nile-markdown>`
10
+ );
11
+ expect(el).to.exist;
12
+ });
13
+
14
+ it('2. should have a shadow root', async () => {
15
+ const el = await fixture<NileMarkdown>(
16
+ html`<nile-markdown></nile-markdown>`
17
+ );
18
+ expect(el.shadowRoot).to.not.be.null;
19
+ });
20
+
21
+ it('3. should have base part', async () => {
22
+ const el = await fixture<NileMarkdown>(
23
+ html`<nile-markdown></nile-markdown>`
24
+ );
25
+ const base = el.shadowRoot!.querySelector('[part~="base"]');
26
+ expect(base).to.exist;
27
+ });
28
+
29
+ it('4. should be instance of NileMarkdown', async () => {
30
+ const el = await fixture<NileMarkdown>(
31
+ html`<nile-markdown></nile-markdown>`
32
+ );
33
+ expect(el).to.be.instanceOf(NileMarkdown);
34
+ });
35
+
36
+ it('5. should have correct tag name', async () => {
37
+ const el = await fixture<NileMarkdown>(
38
+ html`<nile-markdown></nile-markdown>`
39
+ );
40
+ expect(el.tagName.toLowerCase()).to.equal('nile-markdown');
41
+ });
42
+
43
+ // === DEFAULT PROPERTIES ===
44
+ it('6. should have value default to empty string', async () => {
45
+ const el = await fixture<NileMarkdown>(
46
+ html`<nile-markdown></nile-markdown>`
47
+ );
48
+ expect(el.value).to.equal('');
49
+ });
50
+
51
+ it('7. should have tabSize default to 4', async () => {
52
+ const el = await fixture<NileMarkdown>(
53
+ html`<nile-markdown></nile-markdown>`
54
+ );
55
+ expect(el.tabSize).to.equal(4);
56
+ });
57
+
58
+ // === VALUE PROPERTY ===
59
+ it('8. should render markdown from value property', async () => {
60
+ const el = await fixture<NileMarkdown>(
61
+ html`<nile-markdown value="# Heading"></nile-markdown>`
62
+ );
63
+ await el.updateComplete;
64
+ const h1 = el.shadowRoot!.querySelector('h1');
65
+ expect(h1).to.exist;
66
+ expect(h1!.textContent).to.equal('Heading');
67
+ });
68
+
69
+ it('9. should render bold text', async () => {
70
+ const el = await fixture<NileMarkdown>(
71
+ html`<nile-markdown value="This is **bold**"></nile-markdown>`
72
+ );
73
+ await el.updateComplete;
74
+ const strong = el.shadowRoot!.querySelector('strong');
75
+ expect(strong).to.exist;
76
+ expect(strong!.textContent).to.equal('bold');
77
+ });
78
+
79
+ it('10. should render links', async () => {
80
+ const el = await fixture<NileMarkdown>(
81
+ html`<nile-markdown value="[link](https://example.com)"></nile-markdown>`
82
+ );
83
+ await el.updateComplete;
84
+ const a = el.shadowRoot!.querySelector('a');
85
+ expect(a).to.exist;
86
+ expect(a!.getAttribute('href')).to.equal('https://example.com');
87
+ });
88
+
89
+ it('11. should render lists', async () => {
90
+ const el = await fixture<NileMarkdown>(
91
+ html`<nile-markdown></nile-markdown>`
92
+ );
93
+ el.value = '- one\n- two\n- three';
94
+ await el.updateComplete;
95
+ const items = el.shadowRoot!.querySelectorAll('li');
96
+ expect(items.length).to.equal(3);
97
+ });
98
+
99
+ it('12. should render code blocks', async () => {
100
+ const el = await fixture<NileMarkdown>(
101
+ html`<nile-markdown></nile-markdown>`
102
+ );
103
+ el.value = '```\nconst a = 1;\n```';
104
+ await el.updateComplete;
105
+ const pre = el.shadowRoot!.querySelector('pre code');
106
+ expect(pre).to.exist;
107
+ });
108
+
109
+ it('13. should render GFM tables', async () => {
110
+ const el = await fixture<NileMarkdown>(
111
+ html`<nile-markdown></nile-markdown>`
112
+ );
113
+ el.value = '| a | b |\n| - | - |\n| 1 | 2 |';
114
+ await el.updateComplete;
115
+ const table = el.shadowRoot!.querySelector('table');
116
+ expect(table).to.exist;
117
+ });
118
+
119
+ it('14. should render blockquotes', async () => {
120
+ const el = await fixture<NileMarkdown>(
121
+ html`<nile-markdown></nile-markdown>`
122
+ );
123
+ el.value = '> quoted';
124
+ await el.updateComplete;
125
+ const quote = el.shadowRoot!.querySelector('blockquote');
126
+ expect(quote).to.exist;
127
+ });
128
+
129
+ // === SCRIPT CHILD ===
130
+ it('15. should render markdown from a script child', async () => {
131
+ const el = await fixture<NileMarkdown>(html`
132
+ <nile-markdown>
133
+ <script type="text/markdown">
134
+ # From Script
135
+ </script>
136
+ </nile-markdown>
137
+ `);
138
+ await el.updateComplete;
139
+ const h1 = el.shadowRoot!.querySelector('h1');
140
+ expect(h1).to.exist;
141
+ expect(h1!.textContent).to.equal('From Script');
142
+ });
143
+
144
+ it('16. should normalize indented script content', async () => {
145
+ const el = await fixture<NileMarkdown>(html`
146
+ <nile-markdown>
147
+ <script type="text/markdown">
148
+ # Title
149
+
150
+ Paragraph text, indented to match the HTML.
151
+ </script>
152
+ </nile-markdown>
153
+ `);
154
+ await el.updateComplete;
155
+ // Indented content must not be treated as a code block
156
+ expect(el.shadowRoot!.querySelector('pre')).to.not.exist;
157
+ expect(el.shadowRoot!.querySelector('h1')).to.exist;
158
+ expect(el.shadowRoot!.querySelector('p')).to.exist;
159
+ });
160
+
161
+ it('17. value should take precedence over script child', async () => {
162
+ const el = await fixture<NileMarkdown>(html`
163
+ <nile-markdown value="# From Value">
164
+ <script type="text/markdown">
165
+ # From Script
166
+ </script>
167
+ </nile-markdown>
168
+ `);
169
+ await el.updateComplete;
170
+ const h1 = el.shadowRoot!.querySelector('h1');
171
+ expect(h1!.textContent).to.equal('From Value');
172
+ });
173
+
174
+ // === DYNAMIC UPDATES ===
175
+ it('18. should re-render when value changes', async () => {
176
+ const el = await fixture<NileMarkdown>(
177
+ html`<nile-markdown value="# One"></nile-markdown>`
178
+ );
179
+ await el.updateComplete;
180
+ el.value = '# Two';
181
+ await el.updateComplete;
182
+ await el.updateComplete;
183
+ const h1 = el.shadowRoot!.querySelector('h1');
184
+ expect(h1!.textContent).to.equal('Two');
185
+ });
186
+
187
+ it('19. should re-render when script content changes', async () => {
188
+ const el = await fixture<NileMarkdown>(html`
189
+ <nile-markdown>
190
+ <script type="text/markdown">
191
+ # Before
192
+ </script>
193
+ </nile-markdown>
194
+ `);
195
+ await el.updateComplete;
196
+ const script = el.querySelector('script')!;
197
+ script.textContent = '# After';
198
+ // MutationObserver fires async
199
+ await new Promise(resolve => setTimeout(resolve, 0));
200
+ await el.updateComplete;
201
+ const h1 = el.shadowRoot!.querySelector('h1');
202
+ expect(h1!.textContent).to.equal('After');
203
+ });
204
+
205
+ it('20. renderMarkdown() should re-parse manually', async () => {
206
+ const el = await fixture<NileMarkdown>(
207
+ html`<nile-markdown value="# Manual"></nile-markdown>`
208
+ );
209
+ await el.updateComplete;
210
+ el.renderMarkdown();
211
+ await el.updateComplete;
212
+ expect(el.shadowRoot!.querySelector('h1')!.textContent).to.equal('Manual');
213
+ });
214
+
215
+ // === EVENTS ===
216
+ it('21. should emit nile-markdown-rendered after rendering', async () => {
217
+ const el = document.createElement('nile-markdown') as NileMarkdown;
218
+ const listener = oneEvent(el, 'nile-markdown-rendered');
219
+ el.value = '# Event';
220
+ document.body.appendChild(el);
221
+ const event = await listener;
222
+ expect(event).to.exist;
223
+ el.remove();
224
+ });
225
+
226
+ // === STATIC API ===
227
+ it('22. getMarked() should return a shared instance', () => {
228
+ const a = NileMarkdown.getMarked();
229
+ const b = NileMarkdown.getMarked();
230
+ expect(a).to.equal(b);
231
+ });
232
+
233
+ it('23. updateAll() should not throw', async () => {
234
+ await fixture<NileMarkdown>(
235
+ html`<nile-markdown value="# A"></nile-markdown>`
236
+ );
237
+ expect(() => NileMarkdown.updateAll()).to.not.throw();
238
+ });
239
+
240
+ // === EDGE CASES ===
241
+ it('24. should handle empty content', async () => {
242
+ const el = await fixture<NileMarkdown>(
243
+ html`<nile-markdown></nile-markdown>`
244
+ );
245
+ const base = el.shadowRoot!.querySelector('[part~="base"]');
246
+ expect(base!.textContent!.trim()).to.equal('');
247
+ });
248
+
249
+ it('25. should have static styles', () => {
250
+ expect(NileMarkdown.styles).to.exist;
251
+ });
252
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { html } from 'lit';
9
+ import { customElement, property, state } from 'lit/decorators.js';
10
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
11
+ import { Marked } from 'marked';
12
+ import { styles } from './nile-markdown.css';
13
+ import NileElement from '../internal/nile-element';
14
+ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
15
+
16
+ /**
17
+ * Nile markdown component.
18
+ *
19
+ * @tag nile-markdown
20
+ */
21
+
22
+ /**
23
+ * @summary Renders markdown content as HTML in the browser using GitHub Flavored Markdown.
24
+ * @status experimental
25
+ *
26
+ * Content can be provided either through the `value` property or through a
27
+ * `<script type="text/markdown">` element placed as a direct child:
28
+ *
29
+ * ```html
30
+ * <nile-markdown>
31
+ * <script type="text/markdown">
32
+ * # Hello
33
+ * This is **markdown**.
34
+ * </script>
35
+ * </nile-markdown>
36
+ * ```
37
+ *
38
+ * Leading whitespace common to all lines is stripped, so the markdown can be
39
+ * indented to match the surrounding HTML without rendering as a code block.
40
+ *
41
+ * All instances share a single Marked parser. Use `NileMarkdown.getMarked()`
42
+ * to customize it (extensions, renderers, etc.) and `NileMarkdown.updateAll()`
43
+ * to re-render existing instances after changing the configuration.
44
+ *
45
+ * WARNING: The markdown is converted to HTML without sanitization. Do not
46
+ * render unsanitized user input, as this can expose users to XSS attacks.
47
+ *
48
+ * @event nile-markdown-rendered - Emitted after the markdown has been parsed and rendered.
49
+ *
50
+ * @csspart base - The component's base wrapper containing the rendered HTML.
51
+ */
52
+
53
+ @customElement('nile-markdown')
54
+ export class NileMarkdown extends NileElement {
55
+ static styles: CSSResultGroup = styles;
56
+
57
+ /** The shared Marked instance used by every nile-markdown on the page. */
58
+ private static marked: Marked | undefined;
59
+
60
+ /** All connected instances, used by `updateAll()`. */
61
+ private static instances = new Set<NileMarkdown>();
62
+
63
+ /**
64
+ * Returns the shared Marked instance so it can be configured with custom
65
+ * options, extensions, or renderers. Changes affect all instances; call
66
+ * `NileMarkdown.updateAll()` afterwards to re-render existing ones.
67
+ */
68
+ static getMarked(): Marked {
69
+ if (!NileMarkdown.marked) {
70
+ NileMarkdown.marked = new Marked({ gfm: true, async: false });
71
+ }
72
+ return NileMarkdown.marked;
73
+ }
74
+
75
+ /** Re-renders every connected nile-markdown instance. */
76
+ static updateAll(): void {
77
+ NileMarkdown.instances.forEach(instance => instance.renderMarkdown());
78
+ }
79
+
80
+ /**
81
+ * The markdown to render. Takes precedence over a
82
+ * `<script type="text/markdown">` child when set.
83
+ */
84
+ @property() value = '';
85
+
86
+ /** Number of spaces a tab is converted to during whitespace normalization. */
87
+ @property({ type: Number, attribute: 'tab-size' }) tabSize = 4;
88
+
89
+ @state() private renderedHtml = '';
90
+
91
+ /** Re-renders automatically when the markdown script child changes. */
92
+ private mutationObserver = new MutationObserver(() => this.renderMarkdown());
93
+
94
+ connectedCallback(): void {
95
+ super.connectedCallback();
96
+ NileMarkdown.instances.add(this);
97
+ this.mutationObserver.observe(this, {
98
+ childList: true,
99
+ subtree: true,
100
+ characterData: true,
101
+ });
102
+ this.renderMarkdown();
103
+ }
104
+
105
+ disconnectedCallback(): void {
106
+ this.mutationObserver.disconnect();
107
+ NileMarkdown.instances.delete(this);
108
+ super.disconnectedCallback();
109
+ }
110
+
111
+ protected willUpdate(changed: PropertyValues): void {
112
+ super.willUpdate(changed);
113
+ if (changed.has('value') || changed.has('tabSize')) {
114
+ this.computeHtml();
115
+ }
116
+ }
117
+
118
+ protected updated(changed: PropertyValues): void {
119
+ super.updated(changed);
120
+ if (changed.has('renderedHtml')) {
121
+ this.emit('nile-markdown-rendered', undefined, true, true);
122
+ }
123
+ }
124
+
125
+ /** Reads the markdown source from `value` or the script child. */
126
+ private getSource(): string {
127
+ if (this.value) return this.value;
128
+ const script = this.querySelector(':scope > script[type="text/markdown"]');
129
+ return script?.textContent ?? '';
130
+ }
131
+
132
+ /**
133
+ * Converts tabs to spaces and removes the leading indentation common to all
134
+ * lines, so markdown indented to match the HTML doesn't render as code.
135
+ */
136
+ private normalizeWhitespace(text: string): string {
137
+ const tab = ' '.repeat(Math.max(1, this.tabSize));
138
+ const lines = text.replace(/\t/g, tab).split('\n');
139
+
140
+ let minIndent = Infinity;
141
+ for (const line of lines) {
142
+ if (!line.trim()) continue;
143
+ const indent = line.length - line.trimStart().length;
144
+ minIndent = Math.min(minIndent, indent);
145
+ }
146
+ if (!Number.isFinite(minIndent) || minIndent === 0) {
147
+ return text.trim();
148
+ }
149
+ return lines
150
+ .map(line => line.slice(minIndent))
151
+ .join('\n')
152
+ .trim();
153
+ }
154
+
155
+ /** Parses the markdown source into `renderedHtml`. */
156
+ private computeHtml(): void {
157
+ const source = this.normalizeWhitespace(this.getSource());
158
+ this.renderedHtml = NileMarkdown.getMarked().parse(source) as string;
159
+ }
160
+
161
+ /** Parses the markdown source and renders the resulting HTML. */
162
+ renderMarkdown(): void {
163
+ this.computeHtml();
164
+ }
165
+
166
+ render(): TemplateResult {
167
+ return html`
168
+ <div class="markdown" part="base">${unsafeHTML(this.renderedHtml)}</div>
169
+ `;
170
+ }
171
+ }
172
+
173
+ export default NileMarkdown;
174
+
175
+ declare global {
176
+ interface HTMLElementTagNameMap {
177
+ 'nile-markdown': NileMarkdown;
178
+ }
179
+ }
@@ -0,0 +1 @@
1
+ export { NileMarkdownEditor } from './nile-markdown-editor';