@reldens/cms 0.20.0 → 0.21.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.
Files changed (50) hide show
  1. package/README.md +236 -11
  2. package/admin/reldens-admin-client.css +75 -21
  3. package/admin/reldens-admin-client.js +108 -133
  4. package/admin/templates/clear-all-cache-button.html +7 -7
  5. package/admin/templates/edit.html +7 -0
  6. package/admin/templates/layout.html +15 -9
  7. package/admin/templates/list-content.html +4 -2
  8. package/admin/templates/list.html +24 -8
  9. package/admin/templates/view.html +21 -0
  10. package/lib/admin-manager/admin-filters-manager.js +177 -0
  11. package/lib/admin-manager/contents-builder.js +1 -0
  12. package/lib/admin-manager/default-translations.js +38 -0
  13. package/lib/admin-manager/router-contents.js +50 -45
  14. package/lib/admin-manager/router.js +19 -0
  15. package/lib/frontend/content-renderer.js +178 -0
  16. package/lib/frontend/entity-access-manager.js +63 -0
  17. package/lib/frontend/request-processor.js +128 -0
  18. package/lib/frontend/response-manager.js +54 -0
  19. package/lib/frontend/template-cache.js +102 -0
  20. package/lib/frontend/template-resolver.js +111 -0
  21. package/lib/frontend.js +89 -630
  22. package/lib/manager.js +25 -12
  23. package/lib/search-renderer.js +15 -7
  24. package/lib/search-request-handler.js +67 -0
  25. package/lib/search.js +13 -1
  26. package/lib/template-engine/collections-single-transformer.js +11 -5
  27. package/lib/template-engine/collections-transformer.js +47 -34
  28. package/lib/template-engine/entities-transformer.js +3 -2
  29. package/lib/template-engine/partials-transformer.js +5 -6
  30. package/lib/template-engine/system-variables-provider.js +4 -1
  31. package/lib/template-engine.js +11 -5
  32. package/lib/template-reloader.js +307 -0
  33. package/package.json +4 -4
  34. package/templates/{browserconfig.xml → assets/favicons/default/browserconfig.xml} +1 -1
  35. package/templates/assets/favicons/default/favicon.ico +0 -0
  36. package/templates/{site.webmanifest → assets/favicons/default/site.webmanifest} +3 -3
  37. package/templates/js/functions.js +144 -0
  38. package/templates/js/scripts.js +5 -0
  39. package/templates/page.html +11 -5
  40. package/templates/partials/pagedCollection.html +1 -1
  41. package/lib/admin-translations.js +0 -56
  42. package/templates/favicon.ico +0 -0
  43. /package/templates/assets/favicons/{android-icon-144x144.png → default/android-icon-144x144.png} +0 -0
  44. /package/templates/assets/favicons/{android-icon-192x192.png → default/android-icon-192x192.png} +0 -0
  45. /package/templates/assets/favicons/{android-icon-512x512.png → default/android-icon-512x512.png} +0 -0
  46. /package/templates/assets/favicons/{apple-touch-icon.png → default/apple-touch-icon.png} +0 -0
  47. /package/templates/assets/favicons/{favicon-16x16.png → default/favicon-16x16.png} +0 -0
  48. /package/templates/assets/favicons/{favicon-32x32.png → default/favicon-32x32.png} +0 -0
  49. /package/templates/assets/favicons/{mstile-150x150.png → default/mstile-150x150.png} +0 -0
  50. /package/templates/assets/favicons/{safari-pinned-tab.svg → default/safari-pinned-tab.svg} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Reldens CMS
4
4
 
5
- A powerful, flexible Content Management System built with Node.js, featuring an admin panel, multi-domain frontend support, enhanced templating with reusable content blocks, system variables, internationalization, and automated installation.
5
+ A powerful, flexible Content Management System built with Node.js, featuring an admin panel, multi-domain frontend support, enhanced templating with reusable content blocks, system variables, internationalization, template reloading, and automated installation.
6
6
 
7
7
  ## Features
8
8
 
@@ -29,6 +29,8 @@ A powerful, flexible Content Management System built with Node.js, featuring an
29
29
  - **Template functions** for URLs, assets, dates, and translations
30
30
  - **Event-driven rendering** with hooks for customization
31
31
  - **Custom 404 handling**
32
+ - **Advanced search functionality** with template data support
33
+ - **Template reloading** for development with configurable reload strategies
32
34
 
33
35
  ### - Admin Panel
34
36
  - **Full CRUD operations** for all entities including content blocks
@@ -50,12 +52,46 @@ A powerful, flexible Content Management System built with Node.js, featuring an
50
52
 
51
53
  ### - Configuration & Architecture
52
54
  - **Environment-based configuration** (.env file)
53
- - **Modular service architecture** (Frontend, AdminManager, DataServer, TemplateEngine)
55
+ - **Modular service architecture** with specialized classes for better maintainability
54
56
  - **Event-driven system** with hooks for customization
55
57
  - **Extensible authentication** (database users or custom callbacks)
56
58
  - **File security** with path validation and dangerous key filtering
57
59
  - **Internationalization support** with translation files
58
60
 
61
+ ## Architecture
62
+
63
+ ### Core Classes
64
+ The CMS uses a modular architecture with specialized classes:
65
+
66
+ **Frontend Orchestrator:**
67
+ - `Frontend` - Main orchestrator class that coordinates all frontend operations
68
+
69
+ **Template Management:**
70
+ - `TemplateResolver` - Template discovery and domain resolution
71
+ - `TemplateCache` - Template and partial caching management
72
+ - `TemplateReloader` - Template reloading with file change detection
73
+
74
+ **Request Processing:**
75
+ - `RequestProcessor` - HTTP request routing and path handling
76
+ - `SearchRequestHandler` - Dedicated search request processing
77
+
78
+ **Content Management:**
79
+ - `ContentRenderer` - Content generation and template processing
80
+ - `EntityAccessManager` - Entity access control and loading
81
+
82
+ **Response Handling:**
83
+ - `ResponseManager` - HTTP response handling and caching logic
84
+
85
+ **Template Processing:**
86
+ - `TemplateEngine` - Core template rendering with enhanced context
87
+ - `SystemVariablesProvider` - System variables for templates
88
+
89
+ This architecture follows SOLID principles, providing better:
90
+ - **Testability** - Individual components can be tested in isolation
91
+ - **Maintainability** - Changes to one area don't affect others
92
+ - **Reusability** - Components can be reused in different contexts
93
+ - **Readability** - Smaller, focused classes are easier to understand
94
+
59
95
  ## Installation
60
96
 
61
97
  ### Method 1: Automated Web Installer
@@ -102,6 +138,34 @@ RELDENS_DOMAIN_MAPPING={"dev.example.com":"development"}
102
138
  RELDENS_SITE_KEY_MAPPING={"example.com":"main"}
103
139
  ```
104
140
 
141
+ ### Template Reloading Configuration
142
+ Configure template reloading for development environments:
143
+
144
+ ```javascript
145
+ const cms = new Manager({
146
+ // Development: reload templates on every request when changes detected
147
+ reloadTime: -1,
148
+
149
+ // Production: disable template reloading (default)
150
+ reloadTime: 0,
151
+
152
+ // Interval-based: reload every 5 seconds when changes detected
153
+ reloadTime: 5000
154
+ });
155
+ ```
156
+
157
+ **Template Reloading Options:**
158
+ - **`reloadTime: 0`** (default) - Template reloading disabled. Templates load once at startup.
159
+ - **`reloadTime: -1`** - Reload templates on every request when file changes are detected. Best for active development.
160
+ - **`reloadTime: > 0`** - Check for template changes at specified interval (milliseconds) and reload when needed. Good for development with lower overhead.
161
+
162
+ **How it works:**
163
+ - Tracks file modification times for admin and frontend templates
164
+ - Only reloads templates that have actually changed
165
+ - Automatically updates admin contents and frontend template cache
166
+ - Works with both admin panel templates and frontend templates/partials
167
+ - Zero performance impact when disabled (`reloadTime: 0`)
168
+
105
169
  ### Custom Entity Configuration
106
170
  ```javascript
107
171
  const entityConfig = {
@@ -131,6 +195,70 @@ const cms = new Manager({
131
195
  });
132
196
  ```
133
197
 
198
+ ## Search Functionality
199
+
200
+ ### Basic Search
201
+ ```bash
202
+ # Simple search
203
+ /search?search=technology
204
+
205
+ # Entity-specific search with custom limit
206
+ /search?search=javascript&limit=20
207
+
208
+ # Custom template rendering
209
+ /search?search=news&renderPartial=newsListView&renderLayout=minimal
210
+ ```
211
+
212
+ ### Advanced Search with Template Data
213
+ ```bash
214
+ # Pass custom template variables
215
+ /search?search=articles&templateData[columnsClass]=col-md-4&templateData[showExcerpt]=true
216
+
217
+ # Multiple template variables
218
+ /search?search=technology&templateData[columnsClass]=col-lg-6&templateData[cardClass]=shadow-sm&templateData[showAuthor]=false
219
+ ```
220
+
221
+ ### Search Template Variables
222
+ Templates receive dynamic data through URL parameters:
223
+
224
+ **URL:** `/search?search=tech&templateData[columnsClass]=col-md-6&templateData[showDate]=true`
225
+
226
+ **Template (entriesListView.html):**
227
+ ```html
228
+ <div class="{{columnsClass}}">
229
+ <div class="card">
230
+ <h3>{{row.title}}</h3>
231
+ <p>{{row.content}}</p>
232
+ {{#showDate}}
233
+ <span class="date">{{row.created_at}}</span>
234
+ {{/showDate}}
235
+ </div>
236
+ </div>
237
+ ```
238
+
239
+ **Default Values:**
240
+ - `columnsClass` defaults to `col-lg-6` if not provided or empty
241
+ - Custom variables can be added via `templateData[variableName]=value`
242
+
243
+ ### Search Configuration
244
+ ```javascript
245
+ // Custom search sets in Manager configuration
246
+ const searchSets = {
247
+ articlesSearch: {
248
+ entities: [{
249
+ name: 'articles',
250
+ fields: ['title', 'content', 'summary'],
251
+ relations: 'authors'
252
+ }],
253
+ pagination: {active: true, limit: 15, sortBy: 'created_at', sortDirection: 'desc'}
254
+ }
255
+ };
256
+
257
+ const cms = new Manager({
258
+ searchSets: searchSets
259
+ });
260
+ ```
261
+
134
262
  ## Enhanced Templating System
135
263
 
136
264
  ### System Variables
@@ -171,10 +299,10 @@ Templates support dynamic functions for common operations:
171
299
  <!-- URL generation with current domain -->
172
300
  [url(/articles)] <!-- https://example.com/articles -->
173
301
  [url(/contact#form)] <!-- https://example.com/contact#form -->
302
+ [url(/css/styles.css)] <!-- https://example.com/css/styles.css -->
174
303
 
175
304
  <!-- Asset URLs with domain -->
176
- [asset(/css/styles.css)] <!-- https://example.com/css/styles.css -->
177
- [asset(/images/logo.png)] <!-- https://example.com/images/logo.png -->
305
+ [asset(/assets/images/logo.png)] <!-- https://example.com/images/logo.png -->
178
306
 
179
307
  <!-- Date formatting -->
180
308
  [date()] <!-- Current date with default format -->
@@ -303,10 +431,10 @@ Pagination state is managed via URL query parameters:
303
431
  Create `templates/partials/pagedCollection.html`:
304
432
  ```html
305
433
  <div class="row paginated-contents">
306
- <div class="collection-content col-lg-12 mt-2 mb-2">
434
+ <div class="collection-content col-lg-12">
307
435
  {{&collectionContentForCurrentPage}}
308
436
  </div>
309
- <div class="pagination col-lg-12 mt-2 mb-2">
437
+ <div class="pagination col-lg-12">
310
438
  <ul class="pagination-list">
311
439
  {{#prevPageUrl}}
312
440
  <li><a href="{{prevPageUrl}}" class="page-link">{{&prevPageLabel}}</a></li>
@@ -497,11 +625,11 @@ The CMS uses a two-tier layout system:
497
625
  <head>
498
626
  <title>{{title}}</title>
499
627
  <meta name="description" content="{{description}}"/>
500
- <link href="[asset(/css/styles.css)]" rel="stylesheet"/>
628
+ <link href="[url(/css/styles.css)]" rel="stylesheet"/>
501
629
  </head>
502
630
  <body class="{{siteHandle}}">
503
631
  {{&content}}
504
- <script src="[asset(/js/scripts.js)]"></script>
632
+ <script src="[url(/js/scripts.js)]"></script>
505
633
  </body>
506
634
  </html>
507
635
  ```
@@ -584,6 +712,32 @@ templates/
584
712
 
585
713
  ## Advanced Usage
586
714
 
715
+ ### Template Reloading for Development
716
+ ```javascript
717
+ // Different configurations for development vs production
718
+ const isDevelopment = process.env.NODE_ENV === 'development';
719
+
720
+ const cms = new Manager({
721
+ // Enable aggressive template reloading in development
722
+ reloadTime: isDevelopment ? -1 : 0,
723
+
724
+ // Other development-friendly settings
725
+ cache: !isDevelopment,
726
+
727
+ entityAccess: {
728
+ articles: { public: true, operations: ['read'] },
729
+ cmsPages: { public: true, operations: ['read'] }
730
+ }
731
+ });
732
+ ```
733
+
734
+ **Development Workflow with Template Reloading:**
735
+ 1. Set `reloadTime: -1` for instant template updates
736
+ 2. Edit admin templates in `admin/templates/` - changes appear immediately
737
+ 3. Edit frontend templates in `templates/` - changes appear on next page load
738
+ 4. No server restart needed for template changes
739
+ 5. Switch to `reloadTime: 0` in production for optimal performance
740
+
587
741
  ### Event System
588
742
  The CMS provides hooks for customization through event listeners:
589
743
 
@@ -607,6 +761,11 @@ cms.events.on('reldens.afterContentProcess', (eventData) => {
607
761
  // Modify processed content
608
762
  eventData.processedContent += '\n<!-- Processed at ' + new Date() + ' -->';
609
763
  });
764
+
765
+ // Listen for template reloading events
766
+ cms.events.on('reldens.templateReloader.templatesChanged', (eventData) => {
767
+ console.log('Templates changed:', eventData.changedFiles);
768
+ });
610
769
  ```
611
770
 
612
771
  ### Custom Authentication
@@ -679,11 +838,58 @@ The installer provides checkboxes for:
679
838
  - `isInstalled()` - Check if CMS is installed
680
839
  - `initializeServices()` - Initialize all services
681
840
 
682
- ### Frontend Class
841
+ ### Frontend Architecture Classes
842
+
843
+ #### Frontend Class (Orchestrator)
683
844
  - `initialize()` - Set up frontend routes and templates
684
845
  - `handleRequest(req, res)` - Main request handler
685
- - `findRouteByPath(path)` - Database route lookup
686
- - `findEntityByPath(path)` - Entity-based URL handling
846
+ - `renderRoute(route, domain, res, req)` - Route-based rendering
847
+ - `setupStaticAssets()` - Configure static asset serving
848
+
849
+ #### TemplateResolver Class
850
+ - `findTemplatePath(templateName, domain)` - Template discovery with domain fallback
851
+ - `findLayoutPath(layoutName, domain)` - Layout path resolution
852
+ - `findTemplateByPath(path, domain)` - Template lookup by URL path
853
+ - `resolveDomainToFolder(domain)` - Domain to folder mapping
854
+ - `resolveDomainToSiteKey(domain)` - Domain to site key mapping
855
+
856
+ #### TemplateCache Class
857
+ - `loadPartials()` - Load and cache template partials
858
+ - `setupDomainTemplates()` - Initialize domain-specific templates
859
+ - `getPartialsForDomain(domain)` - Get domain-specific partials with fallback
860
+
861
+ #### TemplateReloader Class
862
+ - `checkAndReloadAdminTemplates()` - Check and reload admin templates when changed
863
+ - `checkAndReloadFrontendTemplates()` - Check and reload frontend templates when changed
864
+ - `trackTemplateFiles(templatesPaths)` - Start tracking template files for changes
865
+ - `shouldReloadAdminTemplates(mappedAdminTemplates)` - Check if admin templates need reloading
866
+ - `shouldReloadFrontendTemplates(templatesPath, templateExtensions)` - Check if frontend templates need reloading
867
+ - `handleAdminTemplateReload(adminManager)` - Complete admin template reload process
868
+ - `handleFrontendTemplateReload(templateCache, templateResolver)` - Complete frontend template reload process
869
+
870
+ #### RequestProcessor Class
871
+ - `findRouteByPath(path, domain)` - Database route lookup
872
+ - `handleRouteRedirect(route, res)` - Handle route redirects
873
+ - `getDomainFromRequest(req)` - Extract domain from request
874
+ - `buildCacheKey(path, req)` - Generate cache keys
875
+
876
+ #### ContentRenderer Class
877
+ - `renderWithTemplateContent(content, data, domain, req, route)` - Main content rendering
878
+ - `generateRouteContent(route, domain, req)` - Route-based content generation
879
+ - `generateTemplateContent(templatePath, domain, req, data)` - Template-based content generation
880
+ - `fetchMetaFields(data)` - Process meta fields for templates
881
+
882
+ #### EntityAccessManager Class
883
+ - `loadEntityAccessRules()` - Load entity access configuration
884
+ - `isEntityAccessible(entityName)` - Check entity accessibility
885
+ - `findEntityByPath(path)` - Entity lookup by URL path
886
+
887
+ #### ResponseManager Class
888
+ - `renderWithCacheHandler(contentGenerator, errorHandler, responseHandler, domain, res, path, req)` - Generic cached response handler
889
+ - `renderNotFound(domain, res, req)` - 404 error handling
890
+
891
+ #### SearchRequestHandler Class
892
+ - `handleSearchRequest(req, res)` - Process search requests with template data support
687
893
 
688
894
  ### TemplateEngine Class
689
895
  - `render(template, data, partials, domain, req, route, currentEntityData)` - Main template rendering with enhanced context
@@ -696,6 +902,11 @@ The installer provides checkboxes for:
696
902
  - `buildCurrentRouteData(route)` - Build route context
697
903
  - `buildCurrentDomainData(domain)` - Build domain context
698
904
 
905
+ ### Search Classes
906
+ - `Search.parseSearchParameters(query)` - Parse search query parameters including templateData
907
+ - `Search.executeSearch(config)` - Execute search with configuration
908
+ - `SearchRenderer.renderSearchResults(searchResults, config, domain, req)` - Render search results with template data
909
+
699
910
  ### AdminManager Class
700
911
  - `setupAdmin()` - Initialize admin panel
701
912
  - `generateListRouteContent()` - Entity list pages
@@ -713,6 +924,20 @@ The installer provides checkboxes for:
713
924
  project/
714
925
  ├── admin/
715
926
  │ └── templates/ # Admin panel templates
927
+ ├── lib/
928
+ │ ├── frontend/ # Frontend specialized classes
929
+ │ │ ├── template-resolver.js
930
+ │ │ ├── template-cache.js
931
+ │ │ ├── request-processor.js
932
+ │ │ ├── entity-access-manager.js
933
+ │ │ ├── content-renderer.js
934
+ │ │ └── response-manager.js
935
+ │ ├── frontend.js # Main Frontend orchestrator
936
+ │ ├── template-reloader.js # Template reloading functionality
937
+ │ ├── search-request-handler.js
938
+ │ ├── search.js # Search functionality
939
+ │ ├── search-renderer.js # Search result rendering
940
+ │ └── template-engine.js # Core template processing
716
941
  ├── templates/
717
942
  │ ├── layouts/ # Body content layouts
718
943
  │ ├── domains/ # Domain-specific templates
@@ -387,7 +387,7 @@
387
387
  .button {
388
388
  margin-bottom: 1rem;
389
389
 
390
- &.list-delete-selection {
390
+ &.list-select {
391
391
  margin-bottom: 0;
392
392
  }
393
393
  }
@@ -532,8 +532,7 @@
532
532
  flex-direction: row;
533
533
  vertical-align: middle;
534
534
  align-items: center;
535
- width: 100%;
536
- margin: 0 0 1rem 0;
535
+ margin: 0 1rem 1rem 0;
537
536
  font-size: 14px;
538
537
  color: var(--darkGrey);
539
538
 
@@ -587,6 +586,59 @@
587
586
  }
588
587
  }
589
588
 
589
+ .filters-header {
590
+ display: flex;
591
+ justify-content: space-between;
592
+ align-items: center;
593
+ width: 100%;
594
+ margin-bottom: 1rem;
595
+ flex-wrap: wrap;
596
+ gap: 1rem;
597
+ }
598
+
599
+ .filters-toggle {
600
+ cursor: pointer;
601
+ margin: 0;
602
+ padding: 0.5rem 0;
603
+ flex-shrink: 0;
604
+ }
605
+
606
+ .filters-toggle img {
607
+ width: 34px;
608
+ height: auto;
609
+ margin-right: 0.5rem;
610
+ }
611
+
612
+ .search-controls {
613
+ display: flex;
614
+ align-items: center;
615
+ gap: 0.5rem;
616
+ flex-wrap: wrap;
617
+ flex: 1;
618
+ justify-content: flex-end;
619
+ }
620
+
621
+ .search-input-group input[type="text"] {
622
+ width: calc(100% - 1rem);
623
+ padding: 0.5rem;
624
+ border: 1px solid #ccc;
625
+ border-radius: 4px;
626
+ font-size: 1rem;
627
+ }
628
+
629
+ .search-input-group input[type="text"]:focus {
630
+ outline: none;
631
+ border-color: var(--lightBlue);
632
+ box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
633
+ }
634
+
635
+ .filter-actions {
636
+ display: flex;
637
+ gap: 0.5rem;
638
+ flex-wrap: wrap;
639
+ flex-shrink: 0;
640
+ }
641
+
590
642
  .maps-wizard {
591
643
  .main-action-container.maps-selection {
592
644
  width: 100%;
@@ -787,12 +839,12 @@
787
839
  margin: 0;
788
840
  padding: 0.5rem;
789
841
  border: none;
790
- background: transparent;
842
+ background-color: transparent;
791
843
 
792
844
  &:not([disabled]) {
793
845
  margin: 0;
794
846
  border: 1px solid #7f8c8d;
795
- background: var(--white);
847
+ background-color: var(--white);
796
848
 
797
849
  &[type="checkbox"] {
798
850
  max-width: max-content;
@@ -806,7 +858,7 @@
806
858
  }
807
859
 
808
860
  .actions {
809
- margin-top: 2rem;
861
+ margin: 2rem 0;
810
862
  text-align: center;
811
863
 
812
864
  & form {
@@ -851,7 +903,7 @@
851
903
  overflow: auto;
852
904
  }
853
905
 
854
- .cache-confirm-dialog {
906
+ .confirm-dialog {
855
907
  border: none;
856
908
  border-radius: 8px;
857
909
  padding: 0;
@@ -860,38 +912,40 @@
860
912
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
861
913
  }
862
914
 
863
- .cache-confirm-dialog::backdrop {
915
+ .confirm-dialog::backdrop {
864
916
  background-color: rgba(0, 0, 0, 0.5);
865
917
  }
866
918
 
867
- .cache-dialog-content {
919
+ .dialog-content {
868
920
  padding: 1rem;
869
921
  }
870
922
 
871
- .cache-dialog-content h5 {
923
+ .dialog-content h5 {
872
924
  margin: 0 0 1rem 0;
873
925
  font-size: 18px;
874
926
  color: var(--darkGrey);
875
927
  }
876
928
 
877
- .cache-dialog-content p {
929
+ .dialog-content p {
878
930
  margin: 0 0 1rem 0;
879
931
  color: var(--darkGrey);
880
932
  }
881
933
 
882
- .cache-warning {
883
- padding: 1rem;
884
- background-color: #fff3cd;
885
- border: 1px solid #ffeaa7;
886
- border-radius: 4px;
887
- color: #856404;
888
- margin-bottom: 1rem;
889
- }
890
-
891
- .cache-dialog-actions {
934
+ .dialog-actions {
892
935
  display: flex;
893
936
  justify-content: flex-end;
894
937
  gap: 1rem;
895
938
  }
896
939
 
897
940
  }
941
+
942
+ @media (max-width: 768px) {
943
+ .reldens-admin-panel .filter-actions {
944
+ flex-direction: column;
945
+ }
946
+
947
+ .reldens-admin-panel .filter-actions .button {
948
+ width: 100%;
949
+ text-align: center;
950
+ }
951
+ }