@fsegurai/marked-extended-tabs 15.0.0
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.
- package/README.md +125 -0
- package/package.json +55 -0
- package/src/constants.js +166 -0
- package/src/index.d.ts +50 -0
- package/src/index.js +39 -0
- package/src/renderer.js +70 -0
- package/src/tokenizer.js +99 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="Marked Extensions Logo" src="https://raw.githubusercontent.com/fsegurai/marked-extensions/main/demo/public/marked-extensions.svg">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/fsegurai/marked-extensions/actions/workflows/release-library.yml">
|
|
7
|
+
<img src="https://github.com/fsegurai/marked-extensions/actions/workflows/release-library.yml/badge.svg"
|
|
8
|
+
alt="Build Status">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://www.npmjs.org/package/@fsegurai/marked-extended-tabs">
|
|
11
|
+
<img src="https://img.shields.io/npm/v/@fsegurai/marked-extended-tabs.svg"
|
|
12
|
+
alt="Latest Release">
|
|
13
|
+
</a>
|
|
14
|
+
<br>
|
|
15
|
+
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/fsegurai/marked-extensions">
|
|
16
|
+
<img alt="Dependency status for repo" src="https://img.shields.io/librariesio/github/fsegurai/marked-extensions">
|
|
17
|
+
<a href="https://opensource.org/licenses/MIT">
|
|
18
|
+
<img alt="GitHub License" src="https://img.shields.io/github/license/fsegurai/marked-extensions">
|
|
19
|
+
</a>
|
|
20
|
+
<br>
|
|
21
|
+
<img alt="Stars" src="https://img.shields.io/github/stars/fsegurai/marked-extensions?style=square&labelColor=343b41"/>
|
|
22
|
+
<img alt="Forks" src="https://img.shields.io/github/forks/fsegurai/marked-extensions?style=square&labelColor=343b41"/>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
**A library of extended typographic features for Marked.js.**
|
|
26
|
+
|
|
27
|
+
`@fsegurai/marked-extended-tabs` is an extensions for Marked.js that adds support for extended typographic characters to easily translate plain ASCII punctuation characters into "smart" typographic punctuation HTML entities.
|
|
28
|
+
|
|
29
|
+
### Table of contents
|
|
30
|
+
|
|
31
|
+
- [Installation](#installation)
|
|
32
|
+
- [Usage](#usage)
|
|
33
|
+
- [Basic Usage](#basic-usage)
|
|
34
|
+
- [Configuration Options](#configuration-options)
|
|
35
|
+
- [More Resources](#more-resources)
|
|
36
|
+
- [Available Extensions](#available-extensions)
|
|
37
|
+
- [Demo Application](#demo-application)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
To add `@fsegurai/marked-extended-tabs` along with Marked.js to your `package.json` use the following commands.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun install @fsegurai/marked-extended-tabs marked@^15.0.0 --save
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Basic Usage
|
|
51
|
+
|
|
52
|
+
Import `@fsegurai/marked-extended-tabs` and apply it to your Marked instance as shown below.
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
import { marked } from "marked";
|
|
56
|
+
import markedExtendedTabs from "@fsegurai/marked-extended-tabs";
|
|
57
|
+
|
|
58
|
+
// or UMD script
|
|
59
|
+
// <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
|
|
60
|
+
// <script src="https://cdn.jsdelivr.net/npm/@fsegurai/marked-extended-tabs/lib/index.umd.js"></script>
|
|
61
|
+
|
|
62
|
+
marked.use(markedExtendedTabs());
|
|
63
|
+
|
|
64
|
+
const exampleMarkdown = `
|
|
65
|
+
:::tabs
|
|
66
|
+
::tab{label="JavaScript"}
|
|
67
|
+
\`\`\`
|
|
68
|
+
console.log("Hello from JS");
|
|
69
|
+
\`\`\`
|
|
70
|
+
::
|
|
71
|
+
|
|
72
|
+
::tab{label="Python"}
|
|
73
|
+
\`\`\`python
|
|
74
|
+
print("Hello from Python")
|
|
75
|
+
\`\`\`
|
|
76
|
+
::
|
|
77
|
+
:::
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
marked.parse(exampleMarkdown);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Configuration Options
|
|
84
|
+
|
|
85
|
+
You can customize the tabs behavior with several options:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// Default behavior (recommended)
|
|
89
|
+
marked.use(markedExtendedTabs());
|
|
90
|
+
|
|
91
|
+
// With specific configuration
|
|
92
|
+
marked.use(
|
|
93
|
+
markedExtendedTabs({}),
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Available Extensions
|
|
98
|
+
|
|
99
|
+
- [Marked Extended Accordion](https://www.npmjs.com/package/@fsegurai/marked-extended-accordion)
|
|
100
|
+
- [Marked Extended Alert](https://www.npmjs.com/package/@fsegurai/marked-extended-alert)
|
|
101
|
+
- [Marked Extended Footnote](https://www.npmjs.com/package/@fsegurai/marked-extended-footnote)
|
|
102
|
+
- [Marked Extended Lists](https://www.npmjs.com/package/@fsegurai/marked-extended-lists)
|
|
103
|
+
- [Marked Extended Spoiler](https://www.npmjs.com/package/@fsegurai/marked-extended-spoiler)
|
|
104
|
+
- [Marked Extended Tables](https://www.npmjs.com/package/@fsegurai/marked-extended-tables)
|
|
105
|
+
- [Marked Extended Tabs](https://www.npmjs.com/package/@fsegurai/marked-extended-tabs)
|
|
106
|
+
- [Marked Extended Timeline](https://www.npmjs.com/package/@fsegurai/marked-extended-timeline)
|
|
107
|
+
- [Marked Extended Typographic](https://www.npmjs.com/package/@fsegurai/marked-extended-typographic)
|
|
108
|
+
|
|
109
|
+
### Demo Application
|
|
110
|
+
|
|
111
|
+
To see all extensions in action, check out the [[DEMO]](https://fsegurai.github.io/marked-extensions).
|
|
112
|
+
|
|
113
|
+
To set up the demo locally, follow the next steps:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/fsegurai/marked-extensions.git
|
|
117
|
+
bun install
|
|
118
|
+
bun start
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This will serve the application locally at [http://[::1]:8000](http://[::1]:8000).
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
Licensed under [MIT](https://opensource.org/licenses/MIT).
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fsegurai/marked-extended-tabs",
|
|
3
|
+
"version": "15.0.0",
|
|
4
|
+
"description": "Extended Tabs for Marked.js",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./src/index.js",
|
|
7
|
+
"browser": "./dist/index.umd.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./src/index.js",
|
|
17
|
+
"require": "./lib/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"parser",
|
|
22
|
+
"markedjs",
|
|
23
|
+
"extension",
|
|
24
|
+
"marked",
|
|
25
|
+
"marked-extended-accordion",
|
|
26
|
+
"marked-extended-alert",
|
|
27
|
+
"marked-extended-footnote",
|
|
28
|
+
"marked-extended-lists",
|
|
29
|
+
"marked-extended-spoiler",
|
|
30
|
+
"marked-extended-tables",
|
|
31
|
+
"marked-extended-tabs",
|
|
32
|
+
"marked-extended-timeline",
|
|
33
|
+
"marked-extended-typographic"
|
|
34
|
+
],
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Fabián Segura",
|
|
37
|
+
"url": "https://www.fsegurai.com/"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"prepare": "rollup -c rollup.config.js",
|
|
42
|
+
"test:types": "tsd -f spec/index.test-d.ts -t src/index.d.ts"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"marked": ">=13 <16"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/fsegurai/marked-extensions.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/fsegurai/marked-extensions/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/fsegurai/marked-extensions#readme"
|
|
55
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Counter for generating unique IDs for tab containers
|
|
4
|
+
export const tabCounter = { value: 0 };
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_OPTIONS = {
|
|
7
|
+
tabsClass: 'marked-extended-tabs-container',
|
|
8
|
+
persistSelection: true,
|
|
9
|
+
storageKey: 'marked-tabs-selection',
|
|
10
|
+
animation: 'fade', // 'fade', 'slide', or 'none'
|
|
11
|
+
autoActivate: true, // Automatically activate first tab if none marked active
|
|
12
|
+
customClass: '', // Additional CSS class for the tabs content container
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates the JavaScript code for tab functionality
|
|
17
|
+
*/
|
|
18
|
+
export function createTabsStyles(tabsContainerId, tabsData, animation) {
|
|
19
|
+
return `<style>
|
|
20
|
+
/* Base tab styling */
|
|
21
|
+
#${tabsContainerId} {
|
|
22
|
+
margin: 1rem 0;
|
|
23
|
+
border: 1px solid #ddd;
|
|
24
|
+
border-radius: 4px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
|
|
27
|
+
/* Hide radio inputs */
|
|
28
|
+
& .marked-extended-tabs-input {
|
|
29
|
+
position: absolute;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Navigation styling */
|
|
35
|
+
& .marked-extended-tabs-nav {
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: space-evenly;
|
|
38
|
+
list-style: none;
|
|
39
|
+
margin: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
overflow-x: auto;
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/* Style tab labels as buttons */
|
|
45
|
+
.marked-extended-tabs-label {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex: 1 1 auto;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
align-items: center;
|
|
50
|
+
flex-direction: row;
|
|
51
|
+
flex-wrap: wrap; /* Allow wrapping on small screens */
|
|
52
|
+
gap: 0.5rem;
|
|
53
|
+
padding: 0.75rem 1rem;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
font-size: 1rem;
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
background: transparent;
|
|
58
|
+
border: none;
|
|
59
|
+
border-bottom: 2px solid transparent;
|
|
60
|
+
transition: all 0.2s ease;
|
|
61
|
+
|
|
62
|
+
/* Tab icon styling */
|
|
63
|
+
.marked-extended-tabs-icon {
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
font-size: 1.2rem;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Responsive tweaks */
|
|
72
|
+
@media (max-width: 768px) {
|
|
73
|
+
.marked-extended-tabs-label {
|
|
74
|
+
font-size: 0.9rem;
|
|
75
|
+
padding: 0.5rem 0.75rem;
|
|
76
|
+
gap: 0.4rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.marked-extended-tabs-icon {
|
|
80
|
+
font-size: 1rem;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (max-width: 480px) {
|
|
85
|
+
.marked-extended-tabs-label {
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
text-align: center;
|
|
88
|
+
padding: 0.5rem;
|
|
89
|
+
font-size: 0.85rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.marked-extended-tabs-icon {
|
|
93
|
+
font-size: 1rem;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Content container */
|
|
99
|
+
& .marked-extended-tabs-content {
|
|
100
|
+
padding: 1rem;
|
|
101
|
+
|
|
102
|
+
& img {
|
|
103
|
+
max-width: 100%;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: auto;
|
|
106
|
+
display: block;
|
|
107
|
+
object-fit: cover;
|
|
108
|
+
margin: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Hide all tab panes by default - CRITICAL */
|
|
112
|
+
& .marked-extended-tabs-content-pane {
|
|
113
|
+
display: none;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* CSS-only tab selection - show selected tab */
|
|
119
|
+
${tabsData
|
|
120
|
+
.map((tab) => {
|
|
121
|
+
const inputId = `input-${tab.id}`;
|
|
122
|
+
const labelId = `label-${tab.id}`;
|
|
123
|
+
const tabId = tab.id;
|
|
124
|
+
|
|
125
|
+
return `
|
|
126
|
+
/* Show tab content when corresponding input is checked */
|
|
127
|
+
#${tabsContainerId} #${inputId}:checked ~ .marked-extended-tabs-content #${tabId} {
|
|
128
|
+
display: block;
|
|
129
|
+
animation: ${animation === 'fade' ? 'tab-fade-in' : animation === 'slide' ? 'tab-slide-in' : 'none'} 0.3s ease-in-out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Style for active tab */
|
|
133
|
+
#${tabsContainerId} #${inputId}:checked ~ .marked-extended-tabs-nav #${labelId} {
|
|
134
|
+
// background: light-dark(#2d333b, #fff);
|
|
135
|
+
background: var(--marked-extended-tabs-selected-background) !important;
|
|
136
|
+
border-bottom: 2px solid var(--md-sys-color-primary) !important;
|
|
137
|
+
font-weight: 600 !important;
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
})
|
|
141
|
+
.join('')}
|
|
142
|
+
|
|
143
|
+
/* Animation keyframes */
|
|
144
|
+
${
|
|
145
|
+
animation === 'fade'
|
|
146
|
+
? `
|
|
147
|
+
@keyframes tab-fade-in {
|
|
148
|
+
from { opacity: 0; }
|
|
149
|
+
to { opacity: 1; }
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
: ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
${
|
|
156
|
+
animation === 'slide'
|
|
157
|
+
? `
|
|
158
|
+
@keyframes tab-slide-in {
|
|
159
|
+
from { transform: translateY(10px); opacity: 0; }
|
|
160
|
+
to { transform: translateY(0); opacity: 1; }
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
: ''
|
|
164
|
+
}
|
|
165
|
+
</style>`;
|
|
166
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MarkedExtension } from 'marked';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the tabs extension
|
|
5
|
+
*/
|
|
6
|
+
export interface TabsOptions {
|
|
7
|
+
/**
|
|
8
|
+
* CSS class for the tabs container
|
|
9
|
+
* @default 'marked-extended-tabs-container'
|
|
10
|
+
*/
|
|
11
|
+
tabsClass?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to persist tab selection between page loads
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
persistSelection?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Key to use for localStorage
|
|
21
|
+
* @default 'marked-tabs-selection'
|
|
22
|
+
*/
|
|
23
|
+
storageKey?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Animation type for tab transitions
|
|
27
|
+
* @default 'fade'
|
|
28
|
+
*/
|
|
29
|
+
animation?: 'fade' | 'slide' | 'none';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Automatically activate first tab if none marked active
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
autoActivate?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Additional CSS class for the tabs content container
|
|
39
|
+
* @default ''
|
|
40
|
+
*/
|
|
41
|
+
customClass?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Adds support for extended tabs in marked.
|
|
46
|
+
* This extension allows creating tabbed content within markdown.
|
|
47
|
+
* @param options Configuration options for the tabs extension
|
|
48
|
+
* @returns A marked extension object that can be passed to marked.use()
|
|
49
|
+
*/
|
|
50
|
+
export default function markedExtendedTabs(options?: TabsOptions): MarkedExtension;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_OPTIONS } from './constants.js';
|
|
4
|
+
import { createTokenizer } from './tokenizer.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds support for extended tabs in marked.
|
|
8
|
+
* This extension allows creating tabbed content within markdown:
|
|
9
|
+
* @param {Object} options - Configuration options
|
|
10
|
+
* @param {string} [options.tabsClass='marked-extended-tabs-container'] - CSS class for the tabs container
|
|
11
|
+
* @param {boolean} [options.persistSelection=true] - Whether to persist tab selection between page loads
|
|
12
|
+
* @param {string} [options.storageKey='marked-tabs-selection'] - Key to use for localStorage
|
|
13
|
+
* @param {string} [options.animation='fade'] - Animation type ('fade', 'slide', or 'none')
|
|
14
|
+
* @param {boolean} [options.autoActivate=true] - Automatically activate the first tab if none is marked active
|
|
15
|
+
* @param {string} [options.customClass=''] - Additional CSS class for the tabs content container
|
|
16
|
+
* @returns {Object} The marked extension object
|
|
17
|
+
*/
|
|
18
|
+
export default function markedExtendedTabs(options = {}) {
|
|
19
|
+
// Validate animation option
|
|
20
|
+
if (options.animation && !['fade', 'slide', 'none'].includes(options.animation)) {
|
|
21
|
+
console.warn(`[marked-extended-tabs] Invalid animation value: ${options.animation}. Using default 'fade'.`);
|
|
22
|
+
options.animation = 'fade';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
26
|
+
|
|
27
|
+
// Ensure storageKey is safe for localStorage
|
|
28
|
+
if (config.persistSelection && (!config.storageKey || typeof config.storageKey !== 'string')) {
|
|
29
|
+
config.storageKey = 'marked-tabs-selection';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
walkTokens(token) {
|
|
34
|
+
// Example: Add custom handling or logging of the tokens
|
|
35
|
+
if (token.type !== 'tabs') return;
|
|
36
|
+
},
|
|
37
|
+
extensions: [createTokenizer(config)],
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/renderer.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import { createTabsStyles } from './constants.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders the tabs component using CSS-only approach (no JavaScript)
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} meta - The tab metadata
|
|
10
|
+
* @param {string} meta.tabsContainerId - Unique ID for the tabs container
|
|
11
|
+
* @param {Array} meta.tabsData - Array of tab data objects
|
|
12
|
+
* @param {string} meta.tabsData[].id - Unique ID for the tab
|
|
13
|
+
* @param {string} meta.tabsData[].content - Content of the tab
|
|
14
|
+
* @param {Object} meta.tabsData[].props - Properties of the tab
|
|
15
|
+
* @param {boolean} meta.tabsData[].props.active - Whether the tab is active
|
|
16
|
+
* @param {string} meta.tabsData[].props.icon - Icon for the tab
|
|
17
|
+
* @param {string} meta.tabsData[].props.label - Label for the tab
|
|
18
|
+
* @param {string} meta.tabsClass - CSS class for the tabs container
|
|
19
|
+
* @param {string} meta.animation - Animation type ('fade', 'slide', or 'none')
|
|
20
|
+
* @param {string} meta.class - Additional CSS class for the tabs
|
|
21
|
+
* @returns {string} The rendered HTML
|
|
22
|
+
*/
|
|
23
|
+
export function renderTabs(meta) {
|
|
24
|
+
const { tabsContainerId, tabsData, tabsClass, animation, customClass = '' } = meta;
|
|
25
|
+
|
|
26
|
+
if (!tabsData || tabsData.length === 0) {
|
|
27
|
+
return '<div class="error-message">No tab content found</div>';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let inputsHtml = '';
|
|
31
|
+
let navHtml = '';
|
|
32
|
+
let contentHtml = '';
|
|
33
|
+
|
|
34
|
+
for (const tab of tabsData) {
|
|
35
|
+
const { id, content, props } = tab;
|
|
36
|
+
const { active, icon, label } = props;
|
|
37
|
+
|
|
38
|
+
const inputId = `input-${id}`;
|
|
39
|
+
const labelId = `label-${id}`;
|
|
40
|
+
const isChecked = active ? 'checked' : '';
|
|
41
|
+
const ariaSelected = active ? 'true' : 'false';
|
|
42
|
+
const iconHtml = icon ? `<span class="marked-extended-tabs-icon">${icon}</span>` : '';
|
|
43
|
+
|
|
44
|
+
// Inputs first
|
|
45
|
+
inputsHtml += `<input type="radio" name="${tabsContainerId}-tabs" id="${inputId}" class="marked-extended-tabs-input" ${isChecked}>`;
|
|
46
|
+
|
|
47
|
+
// Navigation labels
|
|
48
|
+
navHtml += `
|
|
49
|
+
<label for="${inputId}" id="${labelId}" class="marked-extended-tabs-label${customClass}" role="tab" aria-selected="${ariaSelected}" data-tab-id="${id}">
|
|
50
|
+
${iconHtml}<span class="tab-label">${label}</span>
|
|
51
|
+
</label>`;
|
|
52
|
+
|
|
53
|
+
// Tab content
|
|
54
|
+
contentHtml += `
|
|
55
|
+
<div class="marked-extended-tabs-content-pane" id="${id}" role="tabpanel" aria-labelledby="${inputId}">
|
|
56
|
+
${marked.parse(content)}
|
|
57
|
+
</div>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const styles = createTabsStyles(tabsContainerId, tabsData, animation);
|
|
61
|
+
|
|
62
|
+
return `
|
|
63
|
+
<div id="${tabsContainerId}" class="${tabsClass}${customClass}" data-animation="${animation}">
|
|
64
|
+
${inputsHtml}
|
|
65
|
+
<div class="marked-extended-tabs-nav" role="tablist">${navHtml}</div>
|
|
66
|
+
<div class="marked-extended-tabs-content ${customClass}">${contentHtml}</div>
|
|
67
|
+
</div>
|
|
68
|
+
${styles}
|
|
69
|
+
`;
|
|
70
|
+
}
|
package/src/tokenizer.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { constructProps, tabContainerRegex, tabItemRegex, validateRegex } from '../../helper/utils.js';
|
|
4
|
+
import { tabCounter } from './constants.js';
|
|
5
|
+
import { renderTabs } from './renderer.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a tokenizer function for the tabs extension
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} config - Configuration options
|
|
11
|
+
* @returns {Object} The tokenizer object
|
|
12
|
+
*/
|
|
13
|
+
export function createTokenizer(config) {
|
|
14
|
+
const { autoActivate } = config;
|
|
15
|
+
const tabElement = 'tabBlock';
|
|
16
|
+
const tabItemElement = 'tabItemBlock';
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
name: 'tabs',
|
|
20
|
+
level: 'block',
|
|
21
|
+
tokenizer(src) {
|
|
22
|
+
const blockMatch = validateRegex(tabElement, src);
|
|
23
|
+
|
|
24
|
+
if (!blockMatch) return undefined;
|
|
25
|
+
|
|
26
|
+
// First position is the full match, second position is the content
|
|
27
|
+
const [raw, tabContent] = blockMatch;
|
|
28
|
+
|
|
29
|
+
// Generate unique ID for this tab container
|
|
30
|
+
const tabsContainerId = `tabs-container-${++tabCounter.value}`;
|
|
31
|
+
|
|
32
|
+
// Find individual tabs directly in the extracted tab content
|
|
33
|
+
const tabsData = [];
|
|
34
|
+
|
|
35
|
+
let tabMatch;
|
|
36
|
+
let tabIndex = 0;
|
|
37
|
+
|
|
38
|
+
// Find all tab items in the content
|
|
39
|
+
while ((tabMatch = tabItemRegex.exec(tabContent)) !== null) {
|
|
40
|
+
// Extract tab properties and content
|
|
41
|
+
const propsStr = tabMatch[1];
|
|
42
|
+
const tabContent = tabMatch[2].trim();
|
|
43
|
+
|
|
44
|
+
// Parse properties with defaults
|
|
45
|
+
const props = constructProps(tabItemElement, propsStr);
|
|
46
|
+
|
|
47
|
+
// If no label is provided, use a default
|
|
48
|
+
if (!props.label) props.label = `Tab ${tabIndex + 1}`;
|
|
49
|
+
|
|
50
|
+
// Convert string 'true'/'false' to boolean for the active property
|
|
51
|
+
props.active = props.active === 'true';
|
|
52
|
+
|
|
53
|
+
// Add to tabs data
|
|
54
|
+
tabsData.push({
|
|
55
|
+
id: `${tabsContainerId}-tab-${tabIndex}`,
|
|
56
|
+
props,
|
|
57
|
+
content: tabContent,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
tabIndex++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If no tabs were found, return undefined
|
|
64
|
+
if (tabsData.length === 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Apply auto-activation if enabled and no tab is explicitly marked as active
|
|
69
|
+
if (autoActivate) {
|
|
70
|
+
const hasActiveTab = tabsData.some((tab) => tab.props.active === true);
|
|
71
|
+
if (!hasActiveTab && tabsData.length > 0) {
|
|
72
|
+
tabsData[0].props.active = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Parse properties from the raw string if needed (e.g. :::tabs{class="custom"})
|
|
77
|
+
// We'll implement a simple parser for this
|
|
78
|
+
const blockPropMatch = raw.match(tabContainerRegex);
|
|
79
|
+
const blockProps = blockPropMatch && blockPropMatch[1] ? constructProps(tabElement, blockPropMatch[1]) : {};
|
|
80
|
+
|
|
81
|
+
// Create token with metadata for the renderer
|
|
82
|
+
const token = {
|
|
83
|
+
type: 'tabs',
|
|
84
|
+
raw,
|
|
85
|
+
meta: {
|
|
86
|
+
tabsContainerId,
|
|
87
|
+
tabsData,
|
|
88
|
+
...config,
|
|
89
|
+
...blockProps,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return token;
|
|
94
|
+
},
|
|
95
|
+
renderer({ meta }) {
|
|
96
|
+
return renderTabs(meta);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|