@zenithbuild/plugins 0.3.2

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.
@@ -0,0 +1,254 @@
1
+ # =============================================================================
2
+ # Zenith Automated Release Workflow
3
+ # =============================================================================
4
+ # This workflow handles automated releases for all Zenith repositories.
5
+ #
6
+ # TRIGGERS:
7
+ # - Push to 'main' branch (analyzes commits for version bump)
8
+ # - Manual trigger via workflow_dispatch (with optional dry-run mode)
9
+ # - Tag creation (v*) for explicit version releases
10
+ #
11
+ # FEATURES:
12
+ # - Conventional Commits parsing for automatic version determination
13
+ # - Automatic CHANGELOG.md generation
14
+ # - GitHub Release creation
15
+ # - Optional NPM publishing
16
+ # - Commits updated files back to repo
17
+ # - Dry-run mode for testing
18
+ # - Monorepo support (detects changed packages)
19
+ #
20
+ # REQUIRED SECRETS:
21
+ # - NPM_TOKEN: For publishing to NPM (if enabled)
22
+ # - GITHUB_TOKEN: Automatically provided by GitHub Actions
23
+ # =============================================================================
24
+
25
+ name: Release
26
+
27
+ on:
28
+ push:
29
+ branches:
30
+ - main
31
+ tags:
32
+ - 'v*'
33
+ paths-ignore:
34
+ - '**.md'
35
+ - '.github/**'
36
+ - '!.github/workflows/release.yml'
37
+
38
+ workflow_dispatch:
39
+ inputs:
40
+ dry_run:
41
+ description: 'Dry run mode (no actual release)'
42
+ required: false
43
+ default: false
44
+ type: boolean
45
+ package:
46
+ description: 'Specific package to release (for monorepo, leave empty for auto-detect)'
47
+ required: false
48
+ default: ''
49
+ type: string
50
+ bump_type:
51
+ description: 'Force version bump type (leave empty for auto-detect from commits)'
52
+ required: false
53
+ default: ''
54
+ type: choice
55
+ options:
56
+ - ''
57
+ - patch
58
+ - minor
59
+ - major
60
+ publish_npm:
61
+ description: 'Publish to NPM'
62
+ required: false
63
+ default: true
64
+ type: boolean
65
+
66
+ # Prevent concurrent releases
67
+ concurrency:
68
+ group: release-${{ github.ref }}
69
+ cancel-in-progress: false
70
+
71
+ env:
72
+ BUN_VERSION: '1.1.38'
73
+
74
+ jobs:
75
+ # ==========================================================================
76
+ # Detect Changes (for monorepo support)
77
+ # ==========================================================================
78
+ detect-changes:
79
+ name: Detect Changed Packages
80
+ runs-on: ubuntu-latest
81
+ outputs:
82
+ packages: ${{ steps.detect.outputs.packages }}
83
+ has_changes: ${{ steps.detect.outputs.has_changes }}
84
+ steps:
85
+ - name: Checkout Repository
86
+ uses: actions/checkout@v4
87
+ with:
88
+ fetch-depth: 0
89
+ token: ${{ secrets.GITHUB_TOKEN }}
90
+
91
+ - name: Setup Bun
92
+ uses: oven-sh/setup-bun@v2
93
+ with:
94
+ bun-version: ${{ env.BUN_VERSION }}
95
+
96
+ - name: Detect Changed Packages
97
+ id: detect
98
+ run: |
99
+ # First, check if this is a single-package repo (package.json in root)
100
+ if [ -f "./package.json" ]; then
101
+ # Count subdirectories with package.json (excluding node_modules)
102
+ SUB_PACKAGES=$(find . -mindepth 2 -name "package.json" -not -path "*/node_modules/*" | wc -l)
103
+
104
+ if [ "$SUB_PACKAGES" -eq 0 ]; then
105
+ # Single package repo - always release from root
106
+ echo "Single package repository detected"
107
+ echo "packages=[\".\"]" >> $GITHUB_OUTPUT
108
+ echo "has_changes=true" >> $GITHUB_OUTPUT
109
+ exit 0
110
+ fi
111
+ fi
112
+
113
+ # Monorepo detection
114
+ PACKAGES=$(find . -name "package.json" -not -path "*/node_modules/*" -not -path "*/.git/*" | xargs -I {} dirname {} | sed 's|^\./||' | grep -v "^$" | sort -u)
115
+
116
+ # For monorepos, detect which packages changed
117
+ CHANGED_PACKAGES="[]"
118
+ if [ "${{ github.event.inputs.package }}" != "" ]; then
119
+ CHANGED_PACKAGES="[\"${{ github.event.inputs.package }}\"]"
120
+ else
121
+ # Get changed files since last tag or in the current push
122
+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
123
+ if [ -z "$LAST_TAG" ]; then
124
+ CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git ls-files)
125
+ else
126
+ CHANGED_FILES=$(git diff --name-only $LAST_TAG HEAD)
127
+ fi
128
+
129
+ # Match changed files to packages
130
+ CHANGED_PKGS=""
131
+ for pkg in $PACKAGES; do
132
+ if echo "$CHANGED_FILES" | grep -q "^$pkg/"; then
133
+ if [ -z "$CHANGED_PKGS" ]; then
134
+ CHANGED_PKGS="\"$pkg\""
135
+ else
136
+ CHANGED_PKGS="$CHANGED_PKGS, \"$pkg\""
137
+ fi
138
+ fi
139
+ done
140
+ CHANGED_PACKAGES="[$CHANGED_PKGS]"
141
+ fi
142
+
143
+ echo "packages=$CHANGED_PACKAGES" >> $GITHUB_OUTPUT
144
+ if [ "$CHANGED_PACKAGES" = "[]" ]; then
145
+ echo "has_changes=false" >> $GITHUB_OUTPUT
146
+ else
147
+ echo "has_changes=true" >> $GITHUB_OUTPUT
148
+ fi
149
+
150
+
151
+ # ==========================================================================
152
+ # Release Job
153
+ # ==========================================================================
154
+ release:
155
+ name: Release
156
+ needs: detect-changes
157
+ if: needs.detect-changes.outputs.has_changes == 'true'
158
+ runs-on: ubuntu-latest
159
+ permissions:
160
+ contents: write
161
+ packages: write
162
+
163
+ strategy:
164
+ fail-fast: false
165
+ matrix:
166
+ package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
167
+
168
+ steps:
169
+ - name: Checkout Repository
170
+ uses: actions/checkout@v4
171
+ with:
172
+ fetch-depth: 0
173
+ token: ${{ secrets.GITHUB_TOKEN }}
174
+
175
+ - name: Setup Bun
176
+ uses: oven-sh/setup-bun@v2
177
+ with:
178
+ bun-version: ${{ env.BUN_VERSION }}
179
+
180
+ - name: Configure Git
181
+ run: |
182
+ git config user.name "github-actions[bot]"
183
+ git config user.email "github-actions[bot]@users.noreply.github.com"
184
+
185
+ - name: Install Dependencies
186
+ working-directory: ${{ matrix.package }}
187
+ run: bun install
188
+
189
+ - name: Run Release Script
190
+ id: release
191
+ working-directory: ${{ matrix.package }}
192
+ env:
193
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
194
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
195
+ DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
196
+ BUMP_TYPE: ${{ github.event.inputs.bump_type || '' }}
197
+ PUBLISH_NPM: ${{ github.event.inputs.publish_npm || 'true' }}
198
+ run: |
199
+ # Run the Bun release script
200
+ bun run scripts/release.ts
201
+
202
+ - name: Build Package
203
+ if: steps.release.outputs.should_release == 'true'
204
+ working-directory: ${{ matrix.package }}
205
+ run: |
206
+ if bun run build 2>/dev/null; then
207
+ echo "Build completed successfully"
208
+ else
209
+ echo "No build script found or build not required"
210
+ fi
211
+
212
+ - name: Commit Changes
213
+ if: steps.release.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true'
214
+ working-directory: ${{ matrix.package }}
215
+ run: |
216
+ git add CHANGELOG.md package.json
217
+ git commit -m "chore(release): v${{ steps.release.outputs.new_version }} [skip ci]" || echo "No changes to commit"
218
+ git push
219
+
220
+ - name: Create GitHub Release
221
+ if: steps.release.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true'
222
+ uses: softprops/action-gh-release@v2
223
+ with:
224
+ tag_name: v${{ steps.release.outputs.new_version }}
225
+ name: Release v${{ steps.release.outputs.new_version }}
226
+ body_path: ${{ matrix.package }}/RELEASE_NOTES.md
227
+ draft: false
228
+ prerelease: false
229
+ token: ${{ secrets.GITHUB_TOKEN }}
230
+
231
+ - name: Publish to NPM
232
+ if: steps.release.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true' && (github.event.inputs.publish_npm == 'true' || github.event.inputs.publish_npm == '')
233
+ working-directory: ${{ matrix.package }}
234
+ run: |
235
+ # Check if package is not private
236
+ PRIVATE=$(cat package.json | bun -e "console.log(JSON.parse(await Bun.stdin.text()).private || false)")
237
+ if [ "$PRIVATE" = "false" ]; then
238
+ echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
239
+ bun publish --access public || npm publish --access public
240
+ else
241
+ echo "Package is private, skipping NPM publish"
242
+ fi
243
+
244
+ - name: Summary
245
+ run: |
246
+ echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
247
+ echo "" >> $GITHUB_STEP_SUMMARY
248
+ if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
249
+ echo "⚠️ **DRY RUN MODE** - No actual release was created" >> $GITHUB_STEP_SUMMARY
250
+ fi
251
+ echo "" >> $GITHUB_STEP_SUMMARY
252
+ echo "- **Package**: ${{ matrix.package }}" >> $GITHUB_STEP_SUMMARY
253
+ echo "- **Version**: ${{ steps.release.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
254
+ echo "- **Bump Type**: ${{ steps.release.outputs.bump_type }}" >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "types": {
4
+ "feat": {
5
+ "title": "✨ Features",
6
+ "bump": "minor",
7
+ "description": "New features or functionality"
8
+ },
9
+ "fix": {
10
+ "title": "🐛 Bug Fixes",
11
+ "bump": "patch",
12
+ "description": "Bug fixes and corrections"
13
+ },
14
+ "perf": {
15
+ "title": "⚡ Performance Improvements",
16
+ "bump": "patch",
17
+ "description": "Performance optimizations"
18
+ },
19
+ "refactor": {
20
+ "title": "♻️ Code Refactoring",
21
+ "bump": "patch",
22
+ "description": "Code changes that neither fix bugs nor add features"
23
+ },
24
+ "docs": {
25
+ "title": "📚 Documentation",
26
+ "bump": null,
27
+ "description": "Documentation only changes"
28
+ },
29
+ "style": {
30
+ "title": "💄 Styles",
31
+ "bump": null,
32
+ "description": "Code style changes (formatting, whitespace)"
33
+ },
34
+ "test": {
35
+ "title": "✅ Tests",
36
+ "bump": null,
37
+ "description": "Adding or updating tests"
38
+ },
39
+ "build": {
40
+ "title": "📦 Build System",
41
+ "bump": "patch",
42
+ "description": "Build system or dependency changes"
43
+ },
44
+ "ci": {
45
+ "title": "🔧 CI Configuration",
46
+ "bump": null,
47
+ "description": "CI/CD configuration changes"
48
+ },
49
+ "chore": {
50
+ "title": "🔨 Chores",
51
+ "bump": null,
52
+ "description": "Maintenance tasks and other changes"
53
+ },
54
+ "revert": {
55
+ "title": "⏪ Reverts",
56
+ "bump": "patch",
57
+ "description": "Reverting previous commits"
58
+ }
59
+ },
60
+ "skipCI": [
61
+ "[skip ci]",
62
+ "[ci skip]",
63
+ "[no ci]",
64
+ "chore(release)"
65
+ ],
66
+ "tagPrefix": "v",
67
+ "branches": {
68
+ "main": "latest",
69
+ "next": "next",
70
+ "beta": "beta",
71
+ "alpha": "alpha"
72
+ }
73
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.1] - 2026-01-16
9
+
10
+ ### 🐛 Bug Fixes
11
+
12
+ - **release**: use appendFileSync for GitHub Actions output (197ed51)
13
+
14
+ ### 📝 Other Changes
15
+
16
+ -
17
+ 1b01347bc123da83fa3d47244243ccc71bc08119 ()
18
+ -
19
+ 0e46b4719d367aae779989ba8dd2c663a3367f68 ()
20
+ -
21
+ 421a8073322947af590e877d5082422788fc2181 ()
22
+ -
23
+ d897b6b63ce9befc76fca102b26ee9810f96658f ()
24
+ -
25
+ 90fc174c6cc860416a66b3cc0551f31c891fa5e6 ()
26
+ - added hooks and markdown compad (5df2a49)
27
+ -
28
+ ebd3b4562954462bcc7266a92704832175e537c4 ()
29
+ -
30
+ 90ac532ab8af8b4c7df64853522c77feb4c9cb7f ()
31
+ -
32
+ 5bc7dba965edfe06ee95a151447af7ae45821a78 ()
33
+ -
34
+ f83484b99a677b6c76181526efc0aa66a5ba5cb4 ()
35
+ -
36
+ 1d87564d9816f1f42c7f94360707ad7673157ce0 ()
37
+ -
38
+ 5a856fe53f86f2ddc51a974fc29d2fab5e38bd84 ()
39
+ - ()
40
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zenith Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @zenithbuild/plugins ⚡
2
+
3
+ The official plugin ecosystem for the Zenith framework.
4
+
5
+ ## Overview
6
+
7
+ Zenith is designed to be extensible. The `@zenithbuild/plugins` package provides the core plugin system and is the home for shared official plugins that enhance the framework's capabilities.
8
+
9
+ ## Features
10
+
11
+ - **Extensible Hooks**: Tap into different phases of the Zenith lifecycle (build, dev, runtime).
12
+ - **Official Plugins**: A collection of vetted plugins for common tasks (SEO, Analytics, State persistence, etc.).
13
+ - **Simple API**: Focused on ease of use for plugin authors.
14
+
15
+ ## Plugin Example (Preview)
16
+
17
+ ```typescript
18
+ export default function myZenithPlugin() {
19
+ return {
20
+ name: 'my-plugin',
21
+ setup(api) {
22
+ // Tap into the build process
23
+ api.onBuild(() => {
24
+ console.log('Zenith is building!');
25
+ });
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Getting Started
32
+
33
+ Refer to the Zenith documentation for instructions on how to consume and create plugins.
34
+
35
+ ## License
36
+
37
+ MIT
package/bun.lock ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@zenithbuild/plugins",
7
+ "devDependencies": {
8
+ "@types/node": "^25.0.6",
9
+ },
10
+ },
11
+ },
12
+ "packages": {
13
+ "@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="],
14
+
15
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
16
+ }
17
+ }
@@ -0,0 +1,116 @@
1
+ # ⚡ Zenith Content Plugin
2
+
3
+ <div align="center">
4
+ <img src="https://raw.githubusercontent.com/zenithbuild/zenith/main/assets/logos/logo.png" alt="Zenith Logo" width="120" />
5
+ <h3>Scale your Zenith experience with Content</h3>
6
+
7
+ [![Zenith Ecosystem](https://img.shields.io/badge/Zenith-Ecosystem-blue?style=for-the-badge&logo=zenith)](https://github.com/zenithbuild/zenith)
8
+ [![Template: content](https://img.shields.io/badge/Template-content-cyan?style=for-the-badge)](https://github.com/zenithbuild/create-zenith-plugin)
9
+ </div>
10
+
11
+ ---
12
+
13
+ ## 📖 Introduction
14
+
15
+ Welcome to the **content** plugin! 🎯
16
+
17
+ This plugin exists to [Enter specific purpose here: e.g., integrate with Firebase, provide custom theming utilities, etc.]. It has been scaffolded using the **content** pattern, ensuring it integrates seamlessly with the Zenith core while staying flexible for your needs.
18
+
19
+ ### Why use this?
20
+ - **Seamless Integration**: Designed specifically for the Zenith runtime.
21
+ - **Type Safe**: Built with TypeScript from the ground up.
22
+ - **DX Focused**: Includes example stubs to get you running in seconds.
23
+
24
+ ---
25
+
26
+ ## ⚙️ Installation
27
+
28
+ To enable the **content** plugin in your Zenith project:
29
+
30
+ 1. Import the plugin in your `zenith.config.ts`:
31
+
32
+ ```ts
33
+ import { plugin as content } from "./plugins/content";
34
+
35
+ export default {
36
+ // ... other config
37
+ plugins: [
38
+ content({
39
+ /* options */
40
+ })
41
+ ]
42
+ };
43
+ ```
44
+
45
+ 2. Zenith will automatically call the `setup` hook during the initialization phase.
46
+
47
+ ---
48
+
49
+ ## 🛠️ Configuration
50
+
51
+ The plugin accepts an options object. Define your parameters in `types.ts` and handle them in `index.ts`.
52
+
53
+ | Option | Type | Default | Description |
54
+ | :--- | :--- | :--- | :--- |
55
+ | `enabled` | `boolean` | `true` | Toggle the plugin functionality |
56
+ | `apiKey` | `string` | `undefined` | Required for service-based plugins |
57
+
58
+ > [!TIP]
59
+ > **Pro Tip**: Always use environment variables for sensitive options like `apiKey`. Use `process.env.MY_PLUGIN_KEY` in your config!
60
+
61
+ ---
62
+
63
+ ## 🚀 Usage
64
+
65
+ Once configured, the plugin interacts with the Zenith lifecycle via the `setup(ctx)` function.
66
+
67
+ ### Runtime Hooks
68
+ You can access the Zenith context (`ctx`) to:
69
+ - Access the component registry.
70
+ - Inject global styles.
71
+ - Listen to lifecycle events.
72
+
73
+ ```ts
74
+ // Example: Implementation inside index.ts
75
+ setup(ctx) {
76
+ ctx.on('mount', () => {
77
+ console.log("Content is active!");
78
+ });
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🎨 Examples
85
+
86
+ ### Basic Setup
87
+ [Provide a simple use case here]
88
+
89
+ ### Advanced Customization
90
+ [Demonstrate complex logic or hooks interaction here]
91
+
92
+ ---
93
+
94
+ ## 🩺 Troubleshooting & FAQ
95
+
96
+ > [!CAUTION]
97
+ > **Common Pitfall**: If your plugin isn't firing, ensure it's added to the `plugins` array in the *correct* environment config.
98
+
99
+ **Q: Can I use this with other plugins?**
100
+ A: Yes! Zenith plugins are composable. Just be mindful of hook execution order.
101
+
102
+ **Q: Where do my logs go?**
103
+ A: By default, logs from the `setup` hook appear in your dev server console.
104
+
105
+ ---
106
+
107
+ ## 🤝 Contributing
108
+
109
+ We welcome all contributions to the Zenith ecosystem!
110
+ - **Submit a Bug**: Open an issue describing the behavior.
111
+ - **Request a Feature**: Let us know what's missing.
112
+ - **PRs**: Point your PRs to the `main` branch.
113
+
114
+ ---
115
+
116
+ *Generated with [create-zenith-plugin](https://github.com/zenithbuild/create-zenith-plugin)*
@@ -0,0 +1,39 @@
1
+ import type { ContentItem, EnhancerFn } from './types';
2
+
3
+ export const builtInEnhancers: Record<string, EnhancerFn> = {
4
+ readTime: (item: ContentItem) => {
5
+ const wordsPerMinute = 200;
6
+ const text = item.content || '';
7
+ const wordCount = text.split(/\s+/).length;
8
+ const minutes = Math.ceil(wordCount / wordsPerMinute);
9
+ return {
10
+ ...item,
11
+ readTime: `${minutes} min`
12
+ };
13
+ },
14
+ wordCount: (item: ContentItem) => {
15
+ const text = item.content || '';
16
+ const wordCount = text.split(/\s+/).length;
17
+ return {
18
+ ...item,
19
+ wordCount
20
+ };
21
+ }
22
+ };
23
+
24
+ export async function applyEnhancers(item: ContentItem, enhancers: (string | EnhancerFn)[]): Promise<ContentItem> {
25
+ let enrichedItem = { ...item };
26
+ for (const enhancer of enhancers) {
27
+ if (typeof enhancer === 'string') {
28
+ const fn = builtInEnhancers[enhancer];
29
+ if (fn) {
30
+ enrichedItem = await fn(enrichedItem);
31
+ } else {
32
+ console.warn(`Enhancer "${enhancer}" not found.`);
33
+ }
34
+ } else if (typeof enhancer === 'function') {
35
+ enrichedItem = await enhancer(enrichedItem);
36
+ }
37
+ }
38
+ return enrichedItem;
39
+ }