@md-plugins/quasar-app-extension-q-press 0.1.0-beta.16 → 0.1.0-beta.18

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 (114) hide show
  1. package/README.md +5 -5
  2. package/dist/install.js +5 -0
  3. package/dist/templates/init/src/_q-press/api/components/MarkdownCode.json +2 -2
  4. package/dist/templates/init/src/_q-press/components/MarkdownCode.vue +70 -2
  5. package/dist/templates/init/src/_q-press/components/MarkdownCodepen.vue +23 -1
  6. package/dist/templates/init/src/_q-press/components/MarkdownLink.vue +3 -3
  7. package/dist/templates/init/src/_q-press/css/app.scss +16 -6
  8. package/{src/templates/update/src/_q-press/css/prism-theme.scss → dist/templates/init/src/_q-press/css/code-theme.scss} +17 -2
  9. package/dist/templates/init/src/_q-press/css/themes/default.scss +1 -1
  10. package/dist/templates/init/src/_q-press/css/themes/mystic.scss +1 -1
  11. package/dist/templates/init/src/_q-press/css/themes/newspaper.scss +1 -1
  12. package/dist/templates/init/src/_q-press/css/themes/sunrise.scss +1 -1
  13. package/dist/templates/init/src/_q-press/css/themes/tawny.scss +1 -1
  14. package/dist/templates/init/src/components/LandingPage/LandingPage.vue +5 -1
  15. package/dist/templates/init/src/components/page-parts/releases/GitHubReleases.vue +121 -0
  16. package/dist/templates/init/src/components/page-parts/releases/PackageReleases.vue +191 -0
  17. package/dist/templates/init/src/components/page-parts/releases/md-table-parser.ts +52 -0
  18. package/dist/templates/init/src/components/page-parts/releases/sanitize.ts +132 -0
  19. package/dist/templates/init/src/markdown/__elements.md +2 -2
  20. package/dist/templates/init/src/markdown/__elements2.md +2 -2
  21. package/dist/templates/init/src/markdown/getting-started/introduction.md +3 -3
  22. package/dist/templates/init/src/markdown/md-plugins/codeblocks/advanced.md +2 -2
  23. package/dist/templates/init/src/markdown/md-plugins/codeblocks/overview.md +3 -3
  24. package/dist/templates/init/src/markdown/other/contact.md +37 -0
  25. package/dist/templates/init/src/markdown/other/contributing/bugs-and-feature-requests.md +33 -0
  26. package/dist/templates/init/src/markdown/other/contributing/call-to-action.md +25 -0
  27. package/dist/templates/init/src/markdown/other/contributing/documentation.md +31 -0
  28. package/dist/templates/init/src/markdown/other/contributing/overview.md +44 -0
  29. package/dist/templates/init/src/markdown/other/contributing/packages.md +28 -0
  30. package/dist/templates/init/src/markdown/other/contributing/sponsor.md +23 -0
  31. package/dist/templates/init/src/markdown/{guides → other}/faq.md +7 -0
  32. package/dist/templates/init/src/markdown/other/releases.md +11 -0
  33. package/dist/templates/init/src/markdown/{guides → other}/upgrade-guide.md +9 -4
  34. package/dist/templates/init/src/markdown/quasar-app-extensions/qpress/components.md +0 -4
  35. package/dist/templates/init/src/markdown/quasar-app-extensions/qpress/overview.md +5 -5
  36. package/dist/templates/init/src/markdown/quasar-app-extensions/qpress/site-config.md +1 -2
  37. package/dist/templates/init/src/markdown/quasar-app-extensions/qpress/themes.md +89 -8
  38. package/dist/templates/init/src/siteConfig/index.ts +27 -21
  39. package/dist/templates/update/src/_q-press/api/components/MarkdownCode.json +2 -2
  40. package/dist/templates/update/src/_q-press/components/MarkdownCode.vue +70 -2
  41. package/dist/templates/update/src/_q-press/components/MarkdownCodepen.vue +23 -1
  42. package/dist/templates/update/src/_q-press/components/MarkdownLink.vue +3 -3
  43. package/dist/templates/update/src/_q-press/css/app.scss +16 -6
  44. package/dist/templates/update/src/_q-press/css/{prism-theme.scss → code-theme.scss} +17 -2
  45. package/dist/templates/update/src/_q-press/css/themes/default.scss +1 -1
  46. package/dist/templates/update/src/_q-press/css/themes/mystic.scss +1 -1
  47. package/dist/templates/update/src/_q-press/css/themes/newspaper.scss +1 -1
  48. package/dist/templates/update/src/_q-press/css/themes/sunrise.scss +1 -1
  49. package/dist/templates/update/src/_q-press/css/themes/tawny.scss +1 -1
  50. package/package.json +15 -15
  51. package/src/install.ts +6 -0
  52. package/src/templates/init/src/_q-press/api/components/MarkdownCode.json +2 -2
  53. package/src/templates/init/src/_q-press/components/MarkdownCode.vue +70 -2
  54. package/src/templates/init/src/_q-press/components/MarkdownCodepen.vue +23 -1
  55. package/src/templates/init/src/_q-press/components/MarkdownLink.vue +3 -3
  56. package/src/templates/init/src/_q-press/css/app.scss +16 -6
  57. package/{dist/templates/init/src/_q-press/css/prism-theme.scss → src/templates/init/src/_q-press/css/code-theme.scss} +17 -2
  58. package/src/templates/init/src/_q-press/css/themes/default.scss +1 -1
  59. package/src/templates/init/src/_q-press/css/themes/mystic.scss +1 -1
  60. package/src/templates/init/src/_q-press/css/themes/newspaper.scss +1 -1
  61. package/src/templates/init/src/_q-press/css/themes/sunrise.scss +1 -1
  62. package/src/templates/init/src/_q-press/css/themes/tawny.scss +1 -1
  63. package/src/templates/init/src/components/LandingPage/LandingPage.vue +5 -1
  64. package/src/templates/init/src/components/page-parts/releases/GitHubReleases.vue +121 -0
  65. package/src/templates/init/src/components/page-parts/releases/PackageReleases.vue +191 -0
  66. package/src/templates/init/src/components/page-parts/releases/md-table-parser.ts +52 -0
  67. package/src/templates/init/src/components/page-parts/releases/sanitize.ts +132 -0
  68. package/src/templates/init/src/markdown/__elements.md +2 -2
  69. package/src/templates/init/src/markdown/__elements2.md +2 -2
  70. package/src/templates/init/src/markdown/getting-started/introduction.md +3 -3
  71. package/src/templates/init/src/markdown/md-plugins/codeblocks/advanced.md +2 -2
  72. package/src/templates/init/src/markdown/md-plugins/codeblocks/overview.md +3 -3
  73. package/src/templates/init/src/markdown/other/contact.md +37 -0
  74. package/src/templates/init/src/markdown/other/contributing/bugs-and-feature-requests.md +33 -0
  75. package/src/templates/init/src/markdown/other/contributing/call-to-action.md +25 -0
  76. package/src/templates/init/src/markdown/other/contributing/documentation.md +31 -0
  77. package/src/templates/init/src/markdown/other/contributing/overview.md +44 -0
  78. package/src/templates/init/src/markdown/other/contributing/packages.md +28 -0
  79. package/src/templates/init/src/markdown/other/contributing/sponsor.md +23 -0
  80. package/src/templates/init/src/markdown/{guides → other}/faq.md +7 -0
  81. package/src/templates/init/src/markdown/other/releases.md +11 -0
  82. package/src/templates/init/src/markdown/{guides → other}/upgrade-guide.md +9 -4
  83. package/src/templates/init/src/markdown/quasar-app-extensions/qpress/components.md +0 -4
  84. package/src/templates/init/src/markdown/quasar-app-extensions/qpress/overview.md +5 -5
  85. package/src/templates/init/src/markdown/quasar-app-extensions/qpress/site-config.md +1 -2
  86. package/src/templates/init/src/markdown/quasar-app-extensions/qpress/themes.md +89 -8
  87. package/src/templates/init/src/siteConfig/index.ts +27 -21
  88. package/src/templates/update/src/_q-press/api/components/MarkdownCode.json +2 -2
  89. package/src/templates/update/src/_q-press/components/MarkdownCode.vue +70 -2
  90. package/src/templates/update/src/_q-press/components/MarkdownCodepen.vue +23 -1
  91. package/src/templates/update/src/_q-press/components/MarkdownLink.vue +3 -3
  92. package/src/templates/update/src/_q-press/css/app.scss +16 -6
  93. package/src/templates/{init/src/_q-press/css/prism-theme.scss → update/src/_q-press/css/code-theme.scss} +17 -2
  94. package/src/templates/update/src/_q-press/css/themes/default.scss +1 -1
  95. package/src/templates/update/src/_q-press/css/themes/mystic.scss +1 -1
  96. package/src/templates/update/src/_q-press/css/themes/newspaper.scss +1 -1
  97. package/src/templates/update/src/_q-press/css/themes/sunrise.scss +1 -1
  98. package/src/templates/update/src/_q-press/css/themes/tawny.scss +1 -1
  99. package/dist/templates/init/src/_q-press/api/components/MarkdownCodePrism.json +0 -29
  100. package/dist/templates/init/src/_q-press/components/MarkdownCodePrism.ts +0 -36
  101. package/dist/templates/init/src/markdown/guides/contributing.md +0 -101
  102. package/dist/templates/init/src/markdown/guides/release-notes.md +0 -0
  103. package/dist/templates/init/src/markdown/guides/style-guide.md +0 -0
  104. package/dist/templates/init/src/markdown/other/release-notes.md +0 -8
  105. package/dist/templates/update/src/_q-press/api/components/MarkdownCodePrism.json +0 -29
  106. package/dist/templates/update/src/_q-press/components/MarkdownCodePrism.ts +0 -36
  107. package/src/templates/init/src/_q-press/api/components/MarkdownCodePrism.json +0 -29
  108. package/src/templates/init/src/_q-press/components/MarkdownCodePrism.ts +0 -36
  109. package/src/templates/init/src/markdown/guides/contributing.md +0 -101
  110. package/src/templates/init/src/markdown/guides/release-notes.md +0 -0
  111. package/src/templates/init/src/markdown/guides/style-guide.md +0 -0
  112. package/src/templates/init/src/markdown/other/release-notes.md +0 -8
  113. package/src/templates/update/src/_q-press/api/components/MarkdownCodePrism.json +0 -29
  114. package/src/templates/update/src/_q-press/components/MarkdownCodePrism.ts +0 -36
package/README.md CHANGED
@@ -40,12 +40,12 @@ See the [documentation](https://md-plugins.netlify.app/quasar-app-extensions/qpr
40
40
  - `pnpm i -D markdown-it @types/markdown-it`
41
41
  - `bun add -d markdown-it @types/markdown-it`
42
42
 
43
- 3. Add `prismjs` to your project dependencies
43
+ 3. Q-Press adds `shiki` to your project dependencies when invoked. If you are wiring the generated files manually, add it yourself:
44
44
 
45
- - `npm i prismjs`
46
- - `yarn add prismjs`
47
- - `pnpm add prismjs`
48
- - `bun add prismjs`
45
+ - `npm i shiki`
46
+ - `yarn add shiki`
47
+ - `pnpm add shiki`
48
+ - `bun add shiki`
49
49
 
50
50
  ## Modifications
51
51
 
package/dist/install.js CHANGED
@@ -22,6 +22,11 @@ export default defineInstallScript(async (api) => {
22
22
  console.error('----------------------------------');
23
23
  throw new Error('This extension requires TypeScript');
24
24
  }
25
+ api.extendPackageJson({
26
+ dependencies: {
27
+ shiki: '^4.1.0',
28
+ },
29
+ });
25
30
  const path = api.resolve.src('siteConfig');
26
31
  if (existsSync(path)) {
27
32
  // this is an update scenario
@@ -6,7 +6,7 @@
6
6
  "props": {
7
7
  "code": {
8
8
  "type": "String",
9
- "desc": "The code to display",
9
+ "desc": "The code to display with Shiki syntax highlighting",
10
10
  "examples": [
11
11
  "'const a = 1;'",
12
12
  "'<div>Hello World</div>'"
@@ -34,4 +34,4 @@
34
34
  "category": "content"
35
35
  }
36
36
  }
37
- }
37
+ }
@@ -1,14 +1,20 @@
1
1
  <template>
2
2
  <div class="relative-position markdown-copybtn-hover">
3
- <MarkdownCodePrism :lang="props.lang" :code="props.code" :style="style" />
3
+ <pre class="markdown-code" :class="block.className" :style="[style, block.style]"><code v-html="block.code"></code></pre>
4
4
  <MarkdownCopyButton />
5
5
  </div>
6
6
  </template>
7
7
 
8
8
  <script setup lang="ts">
9
+ import { createHighlighterCoreSync } from 'shiki/core'
10
+ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
11
+ import htmlLang from 'shiki/langs/html.mjs'
12
+ import javascriptLang from 'shiki/langs/javascript.mjs'
13
+ import vueLang from 'shiki/langs/vue.mjs'
14
+ import githubDark from 'shiki/themes/github-dark.mjs'
15
+ import githubLight from 'shiki/themes/github-light.mjs'
9
16
  import { computed } from 'vue'
10
17
 
11
- import MarkdownCodePrism from './MarkdownCodePrism'
12
18
  import MarkdownCopyButton from './MarkdownCopyButton.vue'
13
19
 
14
20
  const props = defineProps({
@@ -28,4 +34,66 @@ const props = defineProps({
28
34
  })
29
35
 
30
36
  const style = computed(() => (props.maxHeight !== void 0 ? { maxHeight: props.maxHeight } : null))
37
+
38
+ const block = computed(() => {
39
+ return parseHighlightedBlock(highlightCode(props.code, props.lang))
40
+ })
41
+
42
+ const highlighter = createHighlighterCoreSync({
43
+ engine: createJavaScriptRegexEngine(),
44
+ themes: [githubLight, githubDark],
45
+ langs: [htmlLang, javascriptLang, vueLang].flat(),
46
+ langAlias: {
47
+ bash: 'javascript',
48
+ css: 'html',
49
+ js: 'javascript',
50
+ markup: 'vue',
51
+ sass: 'html',
52
+ scss: 'html',
53
+ sh: 'javascript',
54
+ shell: 'javascript',
55
+ ts: 'javascript',
56
+ },
57
+ })
58
+
59
+ function highlightCode(code: string, lang: string): string {
60
+ const options = {
61
+ lang: normalizeLang(lang),
62
+ themes: {
63
+ light: 'github-light',
64
+ dark: 'github-dark',
65
+ },
66
+ defaultColor: false,
67
+ } as const
68
+
69
+ try {
70
+ return highlighter.codeToHtml(code, options)
71
+ } catch {
72
+ return highlighter.codeToHtml(code, {
73
+ ...options,
74
+ lang: 'text',
75
+ })
76
+ }
77
+ }
78
+
79
+ function normalizeLang(lang: string): string {
80
+ return (
81
+ {
82
+ js: 'javascript',
83
+ markup: 'html',
84
+ ts: 'typescript',
85
+ }[lang] ?? lang
86
+ )
87
+ }
88
+
89
+ function parseHighlightedBlock(html: string): { className: string; code: string; style: string } {
90
+ const match = html.match(/^<pre(?<attrs>[^>]*)><code>(?<code>[\s\S]*)<\/code><\/pre>$/)
91
+ const attrs = match?.groups?.attrs ?? ''
92
+
93
+ return {
94
+ className: attrs.match(/class="(?<value>[^"]*)"/)?.groups?.value ?? 'shiki',
95
+ code: match?.groups?.code ?? '',
96
+ style: attrs.match(/style="(?<value>[^"]*)"/)?.groups?.value ?? '',
97
+ }
98
+ }
31
99
  </script>
@@ -62,9 +62,31 @@ function rewriteRootRelativeUrls(content: string) {
62
62
 
63
63
  function indent(code: string, spaces = 2) {
64
64
  const padding = ' '.repeat(spaces)
65
+ let isInsideTemplateLiteral = false
66
+
65
67
  return code
66
68
  .split('\n')
67
- .map((line) => (line.trim().length > 0 ? padding + line : line))
69
+ .map((line) => {
70
+ const shouldIndent = line.trim().length > 0 && isInsideTemplateLiteral === false
71
+
72
+ for (let index = 0; index < line.length; index++) {
73
+ if (line[index] !== '`') {
74
+ continue
75
+ }
76
+
77
+ let escapeCount = 0
78
+
79
+ for (let escapeIndex = index - 1; escapeIndex >= 0 && line[escapeIndex] === '\\'; escapeIndex--) {
80
+ escapeCount++
81
+ }
82
+
83
+ if (escapeCount % 2 === 0) {
84
+ isInsideTemplateLiteral = !isInsideTemplateLiteral
85
+ }
86
+ }
87
+
88
+ return shouldIndent ? padding + line : line
89
+ })
68
90
  .join('\n')
69
91
  }
70
92
 
@@ -20,14 +20,14 @@ const internal = computed(
20
20
 
21
21
  <style lang="scss">
22
22
  .markdown-link {
23
- color: scale-color($brand-primary, $lightness: -35%);
23
+ color: $brand-light-text;
24
24
  text-decoration: none;
25
- border-bottom: 1px dotted currentColor;
25
+ border-bottom: 1px dotted rgba($brand-primary, 0.78);
26
26
  outline: 0;
27
27
  transition: color $header-quick-transition;
28
28
 
29
29
  body.body--dark & {
30
- color: $brand-primary;
30
+ color: $brand-dark-text;
31
31
  }
32
32
 
33
33
  &:hover {
@@ -1,8 +1,8 @@
1
1
  @use 'sass:math';
2
2
  @use './fonts.scss';
3
3
 
4
- // Keep this import so Prism shares the selected qPress theme variables.
5
- @import './prism-theme.scss';
4
+ // Keep this import so Shiki code blocks share the selected qPress theme variables.
5
+ @import './code-theme.scss';
6
6
 
7
7
  .material-icons {
8
8
  font-family: 'Material Icons';
@@ -83,9 +83,10 @@ ul {
83
83
  line-height: $font-size;
84
84
  border-radius: $generic-border-radius;
85
85
  font-family: inherit;
86
- color: scale-color($brand-primary, $lightness: -35%);
86
+ color: $brand-light-text;
87
+ background-color: rgba($brand-primary, 0.08);
87
88
  vertical-align: baseline;
88
- border: 1px solid currentColor;
89
+ border: 1px solid rgba($brand-primary, 0.5);
89
90
  }
90
91
 
91
92
  .markdown-note {
@@ -108,10 +109,13 @@ ul {
108
109
  // .markdown-note__title,
109
110
  .markdown-link,
110
111
  .markdown-token {
111
- color: scale-color($brand-primary, $lightness: -35%);
112
+ color: $brand-light-text;
113
+ }
114
+ .markdown-link {
115
+ border-bottom-color: rgba($brand-primary, 0.78);
112
116
  }
113
117
  .markdown-token {
114
- border-color: scale-color($brand-primary, $lightness: -35%);
118
+ border-color: rgba($brand-primary, 0.5);
115
119
  }
116
120
  & strong {
117
121
  font-weight: 700;
@@ -553,6 +557,12 @@ $letter-spacing-list: 40, 100, 225, 263, 300, 375, 450;
553
557
  }
554
558
 
555
559
  body.body--dark {
560
+ .markdown-token {
561
+ color: $brand-dark-text;
562
+ background-color: rgba($brand-primary, 0.18);
563
+ border-color: rgba($brand-primary, 0.72);
564
+ }
565
+
556
566
  .markdown-technical {
557
567
  color: $brand-dark-text;
558
568
  background: $brand-dark-bg;
@@ -29,11 +29,16 @@
29
29
  &--copying {
30
30
  .c-lpref,
31
31
  .c-line,
32
- .token.deleted-sign,
33
- .token.prefix {
32
+ .line.diff.remove {
34
33
  display: none;
35
34
  }
36
35
  }
36
+
37
+ .line {
38
+ position: relative;
39
+ display: inline-block;
40
+ width: 100%;
41
+ }
37
42
  }
38
43
 
39
44
  .c-line {
@@ -100,6 +105,11 @@
100
105
  }
101
106
 
102
107
  body.body--light {
108
+ .shiki,
109
+ .shiki span {
110
+ color: var(--shiki-light);
111
+ }
112
+
103
113
  .markdown-code {
104
114
  color: $brand-light-codeblock-text;
105
115
  background-color: $brand-light-codeblock-bg;
@@ -205,6 +215,11 @@ body.body--light {
205
215
  }
206
216
 
207
217
  body.body--dark {
218
+ .shiki,
219
+ .shiki span {
220
+ color: var(--shiki-dark);
221
+ }
222
+
208
223
  .markdown-code {
209
224
  color: $brand-dark-codeblock-text;
210
225
  background-color: $brand-dark-codeblock-bg;
@@ -33,7 +33,7 @@ $light-pill: $brand-light;
33
33
  $light-text: $brand-light-text;
34
34
  $light-bg: $brand-light-bg;
35
35
 
36
- $dark-pill: scale-color($brand-primary, $lightness: -80%);
36
+ $dark-pill: scale-color($brand-dark-bg, $lightness: 12%);
37
37
  $dark-text: $brand-dark-text;
38
38
  $dark-bg: $brand-dark-bg;
39
39
 
@@ -38,7 +38,7 @@ $light-pill: $brand-light;
38
38
  $light-text: $brand-light-text;
39
39
  $light-bg: $brand-light-bg;
40
40
 
41
- $dark-pill: scale-color($brand-primary, $lightness: -80%);
41
+ $dark-pill: scale-color($brand-dark-bg, $lightness: 12%);
42
42
  $dark-text: $brand-dark-text;
43
43
  $dark-bg: $brand-dark-bg;
44
44
 
@@ -34,7 +34,7 @@ $light-pill: $brand-light;
34
34
  $light-text: $brand-light-text;
35
35
  $light-bg: $brand-light-bg;
36
36
 
37
- $dark-pill: scale-color($brand-primary, $lightness: -60%);
37
+ $dark-pill: scale-color($brand-dark-bg, $lightness: 12%);
38
38
  $dark-text: $brand-dark-text;
39
39
  $dark-bg: $brand-dark-bg;
40
40
 
@@ -34,7 +34,7 @@ $light-pill: $brand-light;
34
34
  $light-text: $brand-light-text;
35
35
  $light-bg: $brand-light-bg;
36
36
 
37
- $dark-pill: scale-color($brand-dark, $lightness: 20%);
37
+ $dark-pill: scale-color($brand-dark-bg, $lightness: 12%);
38
38
  $dark-text: $brand-dark-text;
39
39
  $dark-bg: $brand-dark-bg;
40
40
 
@@ -34,7 +34,7 @@ $light-pill: $brand-light;
34
34
  $light-text: $brand-light-text;
35
35
  $light-bg: $brand-light-bg;
36
36
 
37
- $dark-pill: scale-color($brand-primary, $lightness: -80%);
37
+ $dark-pill: scale-color($brand-dark-bg, $lightness: 12%);
38
38
  $dark-text: $brand-dark-text;
39
39
  $dark-bg: $brand-dark-bg;
40
40
 
@@ -26,7 +26,7 @@
26
26
  </span>
27
27
  </router-link>
28
28
  <router-link
29
- to="/guides/upgrade-guide"
29
+ to="/other/upgrade-guide"
30
30
  class="hero-button q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--rounded q-btn--dense"
31
31
  >
32
32
  <span
@@ -54,6 +54,10 @@
54
54
  Markdown Plugins go beyond the standard Markdown syntax.<br />Discover the power of Markdown
55
55
  Plugins and enhance your documentation experience!
56
56
  </p>
57
+ <p>
58
+ Use the Markdown-it and Vite plugins in Vue/Vite projects, or choose the Quasar app
59
+ extensions when you want Q-Press and Quasar CLI Vite integration.
60
+ </p>
57
61
  </div>
58
62
  <div class="row justify-center hero">
59
63
  <div class="hero-title">Markdown-It! Plugins</div>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <q-card flat bordered>
3
+ <q-card-section v-if="error" class="row no-wrap items-center">
4
+ <q-icon name="warning" size="24px" color="negative" class="q-mr-sm" />
5
+ <div>Cannot connect to GitHub. Try again later.</div>
6
+ </q-card-section>
7
+ <q-card-section v-else-if="loading" class="row no-wrap items-center">
8
+ <q-spinner size="24px" color="primary" class="q-mr-sm" />
9
+ <div>Loading release notes from GitHub</div>
10
+ </q-card-section>
11
+ <template v-else>
12
+ <q-separator />
13
+ <q-tab-panels v-model="currentPackage" animated class="packages-container">
14
+ <q-tab-panel
15
+ v-for="(packageReleases, packageName) in packages"
16
+ :key="packageName"
17
+ :name="packageName"
18
+ class="q-pa-none"
19
+ >
20
+ <PackageReleases
21
+ :latest-version="latestVersions[packageName]"
22
+ :releases="packageReleases"
23
+ repo-url="https://github.com/md-plugins/md-plugins"
24
+ />
25
+ </q-tab-panel>
26
+ </q-tab-panels>
27
+ </template>
28
+ </q-card>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { onMounted, ref } from 'vue'
33
+ import { date } from 'quasar'
34
+
35
+ import PackageReleases from './PackageReleases.vue'
36
+ import type { ReleaseInfo } from './PackageReleases.vue'
37
+
38
+ const { extractDate, formatDate } = date
39
+ const packageName = 'MD-Plugins'
40
+
41
+ interface GitHubRelease {
42
+ name?: string
43
+ tag_name?: string
44
+ published_at: string
45
+ body?: string
46
+ }
47
+
48
+ type ReleasePackageMap = Record<string, ReleaseInfo[]>
49
+
50
+ const loading = ref(false)
51
+ const error = ref(false)
52
+ const packages = ref<ReleasePackageMap>({ [packageName]: [] })
53
+ const currentPackage = ref(packageName)
54
+ const latestVersions = ref<Record<string, string>>({})
55
+
56
+ function getReleaseVersion(release: GitHubRelease): string | undefined {
57
+ const name = release.name || release.tag_name || ''
58
+ const match = name.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:[-\w.]+)?)/)
59
+
60
+ return match?.[1] ?? release.tag_name?.replace(/^v/, '')
61
+ }
62
+
63
+ async function queryReleases(): Promise<void> {
64
+ loading.value = true
65
+ error.value = false
66
+
67
+ try {
68
+ const response = await fetch(
69
+ 'https://api.github.com/repos/md-plugins/md-plugins/releases?per_page=100',
70
+ )
71
+
72
+ if (response.ok === false) {
73
+ throw new Error(`GitHub request failed with ${response.status}`)
74
+ }
75
+
76
+ const releases = (await response.json()) as GitHubRelease[]
77
+ const parsedReleases = releases
78
+ .map((release) => {
79
+ const version = getReleaseVersion(release)
80
+
81
+ if (version === undefined) {
82
+ return null
83
+ }
84
+
85
+ return {
86
+ version,
87
+ date: formatDate(extractDate(release.published_at, 'YYYY-MM-DD'), 'YYYY-MM-DD'),
88
+ body: release.body || '',
89
+ label: version,
90
+ }
91
+ })
92
+ .filter((release): release is ReleaseInfo => release !== null)
93
+ .sort((a, b) => {
94
+ return (
95
+ Number.parseInt(b.date.replace(/-/g, ''), 10) -
96
+ Number.parseInt(a.date.replace(/-/g, ''), 10)
97
+ )
98
+ })
99
+
100
+ if (parsedReleases.length === 0) {
101
+ throw new Error('No releases returned from GitHub')
102
+ }
103
+
104
+ packages.value = { [packageName]: parsedReleases }
105
+ latestVersions.value = { [packageName]: parsedReleases[0]?.label ?? '' }
106
+ } catch {
107
+ error.value = true
108
+ } finally {
109
+ loading.value = false
110
+ }
111
+ }
112
+
113
+ onMounted(queryReleases)
114
+ </script>
115
+
116
+ <style lang="scss">
117
+ .packages-container .q-tab-panel {
118
+ padding-right: 0;
119
+ padding-top: 0;
120
+ }
121
+ </style>
@@ -0,0 +1,191 @@
1
+ <template>
2
+ <div>
3
+ <q-input
4
+ v-model="search"
5
+ dense
6
+ square
7
+ borderless
8
+ color="white"
9
+ placeholder="Search..."
10
+ clearable
11
+ class="q-mx-md"
12
+ >
13
+ <template #prepend>
14
+ <q-icon :name="mdiMagnify" />
15
+ </template>
16
+ </q-input>
17
+ <q-separator />
18
+ <q-splitter :model-value="20" :limits="[14, 90]" class="release__splitter">
19
+ <template #before>
20
+ <q-scroll-area>
21
+ <q-tabs
22
+ v-model="selectedVersion"
23
+ vertical
24
+ active-color="primary"
25
+ active-bg-color="blue-1"
26
+ indicator-color="primary"
27
+ >
28
+ <q-tab
29
+ v-for="releaseInfo in filteredReleases"
30
+ :key="releaseInfo.label"
31
+ :name="releaseInfo.label"
32
+ >
33
+ <div class="q-tab__label">{{ releaseInfo.version }}</div>
34
+ <small class="text-grey-7">{{ releaseInfo.date }}</small>
35
+ </q-tab>
36
+ </q-tabs>
37
+ </q-scroll-area>
38
+ </template>
39
+ <template #after>
40
+ <q-tab-panels
41
+ v-model="selectedVersion"
42
+ animated
43
+ transition-prev="slide-down"
44
+ transition-next="slide-up"
45
+ class="releases-container"
46
+ >
47
+ <q-tab-panel
48
+ v-for="releaseInfo in filteredReleases"
49
+ :key="releaseInfo.label"
50
+ :name="releaseInfo.label"
51
+ class="q-pa-none"
52
+ >
53
+ <q-scroll-area>
54
+ <div class="q-pa-md" v-html="currentReleaseBody" />
55
+ </q-scroll-area>
56
+ </q-tab-panel>
57
+ </q-tab-panels>
58
+ </template>
59
+ </q-splitter>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { computed, ref, watch } from 'vue'
65
+ import { mdiMagnify } from '@quasar/extras/mdi-v7'
66
+
67
+ import sanitize from './sanitize'
68
+ import parseMdTable from './md-table-parser'
69
+
70
+ export interface ReleaseInfo {
71
+ version: string
72
+ date: string
73
+ body: string
74
+ label: string
75
+ }
76
+
77
+ const props = withDefaults(
78
+ defineProps<{
79
+ latestVersion?: string | undefined
80
+ releases?: ReleaseInfo[]
81
+ repoUrl?: string
82
+ }>(),
83
+ {
84
+ releases: () => [],
85
+ repoUrl: 'https://github.com/md-plugins/md-plugins',
86
+ },
87
+ )
88
+
89
+ const search = ref('')
90
+ const selectedVersion = ref<string | undefined>(props.latestVersion)
91
+
92
+ watch(
93
+ () => props.latestVersion,
94
+ (val) => {
95
+ selectedVersion.value = val
96
+ },
97
+ )
98
+
99
+ const filteredReleases = computed(() => {
100
+ if (search.value !== '') {
101
+ const val = search.value.toLowerCase()
102
+
103
+ return props.releases.filter((release) => release.body.toLowerCase().includes(val))
104
+ }
105
+
106
+ return props.releases
107
+ })
108
+
109
+ function escapeRegExp(value: string): string {
110
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
111
+ }
112
+
113
+ function parse(body: string): string {
114
+ let content = sanitize(body) + '\n'
115
+
116
+ if (search.value !== '') {
117
+ content = content.replace(
118
+ new RegExp(`(${escapeRegExp(search.value)})`, 'gi'),
119
+ '<span class="bg-accent text-white">$1</span>',
120
+ )
121
+ }
122
+
123
+ content = content
124
+ .replace(/### ([\S ]+)/g, '<div class="text-h6">$1</div>')
125
+ .replace(/## ([\S ]+)/g, '<div class="text-h5">$1</div>')
126
+ .replace(/# ([\S ]+)/g, '<div class="text-h4">$1</div>')
127
+ .replace(/\*\*([\S ]*?)\*\*/g, '<strong>$1</strong>')
128
+ .replace(/\*([\S ]*?)\*/g, '<em>$1</em>')
129
+ .replace(/```([\S]+)/g, '<pre class="markdown-code release__code"><code>')
130
+ .replace(/```\n/g, '</code></pre>')
131
+ .replace(/`(.*?)`/g, '<code class="markdown-token">$1</code>')
132
+ .replace(
133
+ /#([\d]+)/g,
134
+ `<a class="markdown-link" href="${props.repoUrl}/issues/$1" target="_blank">#$1</a>`,
135
+ )
136
+ .replace(/^&gt; ([\S ]+)$/gm, '<div class="release__blockquote">$1</div>')
137
+ .replace(
138
+ /\[([\S ]*?)\]\((\S*?)\)/g,
139
+ '<a class="markdown-link" href="$2" target="_blank">$1</a>',
140
+ )
141
+ .replace(/^ {2}[-*] ([\S .]+)$/gm, '<li class="q-pl-md">$1</li>')
142
+ .replace(/^[-*] ([\S .]+)$/gm, '<li>$1</li>')
143
+ .replace(/<\/li>[\s\n\r]*<li/g, '</li><li')
144
+ .replace(/(<li(?: class="[^"]*")?>.*?<\/li>)+/g, '<ul class="release__list">$&</ul>')
145
+ .replace(/\n/g, '<br>')
146
+
147
+ return content.includes('| -') ? parseMdTable(content) : content
148
+ }
149
+
150
+ const currentReleaseBody = computed(() => {
151
+ const release = props.releases.find((entry) => entry.label === selectedVersion.value)
152
+
153
+ return release ? parse(release.body) : ''
154
+ })
155
+ </script>
156
+
157
+ <style lang="scss">
158
+ .release__splitter .q-scrollarea {
159
+ height: 600px;
160
+ }
161
+
162
+ .release__body {
163
+ white-space: pre-line;
164
+
165
+ .q-markup-table {
166
+ white-space: normal;
167
+ }
168
+ }
169
+
170
+ .release__list {
171
+ margin: 8px 0 16px;
172
+ padding-left: 28px;
173
+
174
+ li {
175
+ margin: 4px 0;
176
+ padding-left: 4px;
177
+ }
178
+ }
179
+
180
+ .release__blockquote {
181
+ background: rgba($primary, 0.05);
182
+ border: 1px solid $primary;
183
+ padding: 4px 8px;
184
+ border-radius: $generic-border-radius;
185
+ }
186
+
187
+ .release__code {
188
+ padding: 4px;
189
+ margin: 8px;
190
+ }
191
+ </style>