@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 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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }