diff --git a/apps/map-viewer/project.json b/apps/map-viewer/project.json index 0ccb1c8bef..a7fade28af 100644 --- a/apps/map-viewer/project.json +++ b/apps/map-viewer/project.json @@ -42,7 +42,7 @@ { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "4kb" + "maximumError": "5kb" } ], "fileReplacements": [ diff --git a/apps/metadata-editor/project.json b/apps/metadata-editor/project.json index e93f92e72b..e38fcc319a 100644 --- a/apps/metadata-editor/project.json +++ b/apps/metadata-editor/project.json @@ -52,7 +52,7 @@ { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "4kb" + "maximumError": "5kb" } ], "outputHashing": "all" diff --git a/apps/search/project.json b/apps/search/project.json index 0fdf04538b..ff98cad88b 100644 --- a/apps/search/project.json +++ b/apps/search/project.json @@ -33,7 +33,7 @@ { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "4kb" + "maximumError": "5kb" } ], "fileReplacements": [ diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 61cb50dd29..ed3b7f3ad5 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -19,3 +19,4 @@ export * from './lib/related-record-card/related-record-card.component' export * from './lib/search-results-error/search-results-error.component' export * from './lib/user-preview/user-preview.component' export * from './lib/record-api-form/record-api-form.component' +export * from './lib/markdown-parser/markdown-parser.component' diff --git a/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.css b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.css new file mode 100644 index 0000000000..36dcdc95d6 --- /dev/null +++ b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.css @@ -0,0 +1,264 @@ +/** Body **/ +:host /deep/ .markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0px 0px 1.5rem 0px; + line-height: 1.5; + word-wrap: break-word; +} + +/** Emphasis **/ + +:host /deep/ .markdown-body strong { + @apply font-bold; + color: var(--color-secondary-darker); +} + +/** Headings **/ + +:host /deep/ .markdown-body h1, +:host /deep/ .markdown-body h2, +:host /deep/ .markdown-body h3, +:host /deep/ .markdown-body h4, +:host /deep/ .markdown-body h5, +:host /deep/ .markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + line-height: 1.25; + @apply text-title font-title font-bold; +} + +:host /deep/ .markdown-body h1 { + margin: 0.67em 0; + padding-bottom: 0.3em; + font-size: 2em; + color: var(--color-primary); +} + +:host /deep/ .markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + color: var(--color-secondary); +} + +:host /deep/ .markdown-body h3 { + font-size: 1.25em; + color: var(--color-secondary); +} + +:host /deep/ .markdown-body h4 { + font-size: 1em; + color: var(--color-secondary); +} + +:host /deep/ .markdown-body h5 { + font-size: 0.875em; + color: var(--color-secondary); +} + +:host /deep/ .markdown-body h6 { + font-size: 0.85em; + color: var(--color-secondary-lighter); +} + +/** Paragraphs **/ + +:host /deep/ .markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +/** Links **/ + +:host /deep/ .markdown-body p > a { + margin-top: 0; + margin-bottom: 10px; + color: var(--color-primary) !important; + text-decoration: none !important; + @apply font-bold; +} + +:host /deep/ .markdown-body p > a:hover { + color: var(--color-primary-darker) !important; +} + +/** Blockquotes **/ + +:host /deep/ .markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: var(--color-secondary-lighter); + border-left: 0.25em solid var(--color-primary-lighter); +} + +/** Code **/ + +:host /deep/ .markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-size: 12px; + background-color: var(--color-gray-100); + word-wrap: normal; +} + +:host /deep/ .markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + color: var(--color-secondary); + border-radius: 6px; +} + +:host /deep/ .markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + border-radius: 6px; +} + +:host /deep/ .markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + border: 0; +} + +/** Horizontal rules **/ + +:host /deep/ .markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid var(--color-secondary); + height: 0.15em; + padding: 0; + margin: 24px 0; + background-color: var(--color-secondary); + border: 0; +} + +:host /deep/ .markdown-body hr::before { + display: table; + content: ''; +} + +:host /deep/ .markdown-body hr::after { + display: table; + clear: both; + content: ''; +} + +/** Lists **/ + +:host /deep/ .markdown-body ul, +:host /deep/ .markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; + list-style: revert; +} + +:host /deep/ .markdown-body ol ol, +:host /deep/ .markdown-body ul ol { + list-style-type: lower-roman; +} + +:host /deep/ .markdown-body ul ul ol, +:host /deep/ .markdown-body ul ol ol, +:host /deep/ .markdown-body ol ul ol, +:host /deep/ .markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +:host /deep/ .markdown-body ol[type='a s'] { + list-style-type: lower-alpha; +} + +:host /deep/ .markdown-body ol[type='A s'] { + list-style-type: upper-alpha; +} + +:host /deep/ .markdown-body ol[type='i s'] { + list-style-type: lower-roman; +} + +:host /deep/ .markdown-body ol[type='I s'] { + list-style-type: upper-roman; +} + +:host /deep/ .markdown-body ol[type='1'] { + list-style: unset; + list-style-type: decimal; +} + +:host /deep/ .markdown-body div > ol:not([type]) { + list-style: unset; + list-style-type: decimal; +} + +/** Table **/ + +:host /deep/ .markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; + padding-bottom: 15px; +} + +:host /deep/ .markdown-body td, +:host /deep/ .markdown-body th { + padding: 0; +} + +:host /deep/ .markdown-body th { + color: var(--color-secondary); +} + +:host /deep/ .markdown-body table th, +:host /deep/ .markdown-body table td { + padding: 6px 13px; + border: 1px solid var(--color-gray-500); +} + +:host /deep/ .markdown-body table td > :last-child { + margin-bottom: 0; +} + +:host /deep/ .markdown-body table tr { + background-color: #ffffff; + border-top: 1px solid var(--color-secondary-lighter); +} + +:host /deep/ .markdown-body table tr:nth-child(2n) { + background-color: var(--color-gray-100); +} + +:host /deep/ .markdown-body table img { + background-color: transparent; +} + +/** Images **/ + +:host /deep/ .markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; + background-color: transparent; +} + +:host /deep/ .markdown-body img[align='right'] { + padding-left: 20px; +} + +:host /deep/ .markdown-body img[align='left'] { + padding-right: 20px; +} diff --git a/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.html b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.html new file mode 100644 index 0000000000..93f466a59a --- /dev/null +++ b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.html @@ -0,0 +1 @@ +
diff --git a/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.spec.ts b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.spec.ts new file mode 100644 index 0000000000..d8534a7bfc --- /dev/null +++ b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MarkdownParserComponent } from './markdown-parser.component' + +describe('MarkdownParserComponent', () => { + let component: MarkdownParserComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MarkdownParserComponent], + }).compileComponents() + + fixture = TestBed.createComponent(MarkdownParserComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should render markdown as HTML', () => { + component.textContent = '**bold markdown**' + fixture.detectChanges() + const markdown = fixture.nativeElement.innerHTML + expect(markdown).toContain('

bold markdown

') + }) + + it('should render HTML as HTML', () => { + component.textContent = '

simple html

' + fixture.detectChanges() + const html = fixture.nativeElement.innerHTML + expect(html).toContain('

simple html

') + }) + + it('should render text as HTML', () => { + component.textContent = 'simple text' + fixture.detectChanges() + const text = fixture.nativeElement.innerHTML + expect(text).toContain('

simple text

') + }) +}) diff --git a/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.stories.ts b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.stories.ts new file mode 100644 index 0000000000..b7664783e7 --- /dev/null +++ b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.stories.ts @@ -0,0 +1,148 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { MarkdownParserComponent } from './markdown-parser.component' + +export default { + title: 'Elements/MarkdownParserComponent', + component: MarkdownParserComponent, + decorators: [ + moduleMetadata({ + declarations: [MarkdownParserComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + textContent: ` +# SUPPORTED MARKDOWN CONTENT + +## 1) Headings + ++ # h1 Heading ++ ## h2 Heading ++ ### h3 Heading ++ #### h4 Heading ++ ##### h5 Heading ++ ###### h6 Heading + + +## 2) Horizontal Rules + ++ ___ + ++ --- + ++ *** + +## 3) Emphasis + ++ **This is bold text** + ++ __This is bold text__ + ++ *This is italic text* + ++ _This is italic text_ + ++ ~~Strikethrough~~ + + +## 3) Blockquotes + + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs right next to each other... +> > > ...or with spaces between arrows. + + +## 4) Lists + +**Unordered** + ++ Create a list by starting a line with +, -, or * ++ Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + * Ac tristique libero volutpat at + + Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit ++ List continue here + +**Ordered** + +1. Lorem ipsum dolor sit amet +2. Consectetur adipiscing elit +3. Integer molestie lorem at massa + + +1. You can use sequential numbers... +1. ...or keep all the numbers as 1. + +**Start numbering with offset:** + +57. foo +1. bar + + +## 5) Code + +**Inline code** + +(no example) + +**Indented code** + + // Some comments + line 1 of code + line 2 of code + line 3 of code + + +**Block code "fences"** + +(no example) + + +## 6) Tables + +**Left aligned columns** + +| Option | Description | +| ------ | ----------- | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +**Right aligned columns** + +| Option | Description | +| ------:| -----------:| +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + + +## 7) Links + +[link text](https://github.com/geonetwork/geonetwork-ui) + +[link with title](https://github.com/geonetwork/geonetwork-ui "title text!") + + +## 8) Images + +**With and without title** + +![Geonetwork](https://geonetwork-opensource.org/_static/chrome/geonetwork3-logo.png "Geonetwork title")`, + }, + argTypes: { + textContent: { + control: 'text', + }, + }, + render: (args) => ({ + props: args, + template: ``, + }), +} diff --git a/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts new file mode 100644 index 0000000000..679462caf8 --- /dev/null +++ b/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts @@ -0,0 +1,16 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core' +import { marked } from 'marked' + +@Component({ + selector: 'gn-ui-markdown-parser', + templateUrl: './markdown-parser.component.html', + styleUrls: ['./markdown-parser.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarkdownParserComponent { + @Input() textContent: string + + get parsedMarkdown() { + return marked.parse(this.textContent) + } +} diff --git a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html index 369eff7235..fa13aae0ba 100644 --- a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +++ b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html @@ -8,11 +8,9 @@
-

+

record.metadata.keywords diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 73b00327a2..eac4060e3b 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -29,6 +29,7 @@ import { GnUiLinkifyDirective } from './metadata-info/linkify.directive' import { PaginationButtonsComponent } from './pagination-buttons/pagination-buttons.component' import { MaxLinesComponent } from './max-lines/max-lines.component' import { RecordApiFormComponent } from './record-api-form/record-api-form.component' +import { MarkdownParserComponent } from './markdown-parser/markdown-parser.component' @NgModule({ imports: [ @@ -65,6 +66,7 @@ import { RecordApiFormComponent } from './record-api-form/record-api-form.compon PaginationButtonsComponent, MaxLinesComponent, RecordApiFormComponent, + MarkdownParserComponent, ], exports: [ MetadataInfoComponent, @@ -85,6 +87,7 @@ import { RecordApiFormComponent } from './record-api-form/record-api-form.compon UserPreviewComponent, PaginationButtonsComponent, RecordApiFormComponent, + MarkdownParserComponent, ], }) export class UiElementsModule {} diff --git a/package-lock.json b/package-lock.json index ff2f2588b7..e1d431a51d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "embla-carousel": "^8.0.0-rc14", "express": "^4.17.1", "geojson-validation": "^1.0.2", + "marked": "^11.1.1", "moment": "^2.29.4", "ng-table-virtual-scroll": "^1.4.1", "ngx-chips": "3.0.0", @@ -23997,6 +23998,17 @@ "react": ">= 0.14.0" } }, + "node_modules/marked": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.1.1.tgz", + "integrity": "sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", diff --git a/package.json b/package.json index d7589bf7cd..7c3bde2dd8 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "embla-carousel": "^8.0.0-rc14", "express": "^4.17.1", "geojson-validation": "^1.0.2", + "marked": "^11.1.1", "moment": "^2.29.4", "ng-table-virtual-scroll": "^1.4.1", "ngx-chips": "3.0.0",