@qwickapps/react-framework 1.3.1 → 1.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.
- package/README.md +17 -1
- package/dist/index.esm.js +116 -33
- package/dist/index.js +116 -37
- package/dist/templates/TemplateResolver.d.ts.map +1 -1
- package/dist/utils/htmlTransform.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +15 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/stories/{form/FormComponents.stories.tsx → FormComponents.stories.tsx} +8 -8
- package/src/templates/TemplateResolver.ts +2 -6
- package/src/utils/__tests__/nested-dom-fix.test.tsx +53 -0
- package/src/utils/__tests__/optional-logging.test.ts +83 -0
- package/src/utils/htmlTransform.tsx +69 -3
- package/src/utils/logger.ts +60 -5
package/README.md
CHANGED
|
@@ -4,7 +4,13 @@ A complete React framework for building modern, responsive applications with int
|
|
|
4
4
|
|
|
5
5
|
## What's New
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### September 1, 2025 - Optional Dependencies & Performance
|
|
8
|
+
- **Optional Logging**: Made `@qwickapps/logging` an optional dependency with console fallback
|
|
9
|
+
- **Reduced Bundle Size**: Core framework no longer includes logging overhead by default
|
|
10
|
+
- **React Error Fixes**: Fixed React error #62 in Markdown mixed content scenarios
|
|
11
|
+
- **DOM Nesting**: Added automatic cleanup of invalid DOM nesting from marked.js output
|
|
12
|
+
|
|
13
|
+
### August 31, 2025 - CMS Content Components
|
|
8
14
|
- **Html Component**: Transform HTML strings into React components with configurable transformation rules
|
|
9
15
|
- **Markdown Component**: Convert Markdown to React components using marked library with Html component integration
|
|
10
16
|
- **Transform System**: Extensible HTML transformation architecture with optimized rule sets for Article and Markdown content
|
|
@@ -52,6 +58,16 @@ A complete React framework for building modern, responsive applications with int
|
|
|
52
58
|
npm install @qwickapps/react-framework
|
|
53
59
|
```
|
|
54
60
|
|
|
61
|
+
### Optional Dependencies
|
|
62
|
+
|
|
63
|
+
The framework uses console-based logging by default. For advanced logging features, optionally install:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @qwickapps/logging
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This will enable structured logging with configurable levels and outputs. If not installed, the framework automatically falls back to console logging.
|
|
70
|
+
|
|
55
71
|
## Quick Start
|
|
56
72
|
|
|
57
73
|
### Basic Setup
|
package/dist/index.esm.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { CircularProgress, Button as Button$1, Paper, Typography, useTheme as useTheme$1, Box, Container as Container$8, Stack, Tooltip, IconButton, Snackbar, Alert, useMediaQuery, Avatar, Chip, Menu, MenuItem, Grid, Divider, Link, ListItemIcon, ListItemText, Card, CardContent, ButtonGroup, Collapse, TextField as TextField$1, FormControl, InputLabel, Select, FormHelperText } from '@mui/material';
|
|
3
3
|
import React, { useMemo, useContext, useState, useEffect, createContext, useRef, useCallback, cloneElement } from 'react';
|
|
4
|
-
import { Logger, createLogger } from '@qwickapps/logging';
|
|
5
|
-
export * from '@qwickapps/logging';
|
|
6
4
|
import { MustacheTemplateProvider, MemoryCacheProvider, CachedDataProvider, Field, Editor, FieldType, Schema, Model, DataType } from '@qwickapps/schema';
|
|
7
5
|
import { IsOptional, IsString, IsIn, IsUrl, IsArray, ValidateNested, IsNumber, Min, Max, IsBoolean, IsNotEmpty, IsObject } from 'class-validator';
|
|
8
6
|
import { Type } from 'class-transformer';
|
|
@@ -468,6 +466,63 @@ function useBaseProps(props) {
|
|
|
468
466
|
};
|
|
469
467
|
}
|
|
470
468
|
|
|
469
|
+
/**
|
|
470
|
+
* QwickApps React Framework - Logger Utility
|
|
471
|
+
*
|
|
472
|
+
* Optional logging with fallback to console when @qwickapps/logging is not available
|
|
473
|
+
*
|
|
474
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
475
|
+
*/
|
|
476
|
+
/**
|
|
477
|
+
* Console-based logger fallback
|
|
478
|
+
*/
|
|
479
|
+
class ConsoleLogger {
|
|
480
|
+
constructor(name) {
|
|
481
|
+
this.name = name;
|
|
482
|
+
}
|
|
483
|
+
debug(message, ...args) {
|
|
484
|
+
{
|
|
485
|
+
console.debug(`[${this.name}] ${message}`, ...args);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
info(message, ...args) {
|
|
489
|
+
console.info(`[${this.name}] ${message}`, ...args);
|
|
490
|
+
}
|
|
491
|
+
warn(message, ...args) {
|
|
492
|
+
console.warn(`[${this.name}] ${message}`, ...args);
|
|
493
|
+
}
|
|
494
|
+
error(message, ...args) {
|
|
495
|
+
console.error(`[${this.name}] ${message}`, ...args);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Create logger with optional @qwickapps/logging dependency
|
|
500
|
+
*/
|
|
501
|
+
function createLogger(name) {
|
|
502
|
+
try {
|
|
503
|
+
// Try to use @qwickapps/logging if available
|
|
504
|
+
const logging = require('@qwickapps/logging');
|
|
505
|
+
return logging.createLogger(name);
|
|
506
|
+
} catch {
|
|
507
|
+
// Fallback to console-based logger
|
|
508
|
+
return new ConsoleLogger(name);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Framework-specific loggers
|
|
513
|
+
*/
|
|
514
|
+
const loggers = {
|
|
515
|
+
scaffold: createLogger('Scaffold'),
|
|
516
|
+
navigation: createLogger('Navigation'),
|
|
517
|
+
auth: createLogger('Auth'),
|
|
518
|
+
theme: createLogger('Theme'),
|
|
519
|
+
palette: createLogger('Palette'),
|
|
520
|
+
form: createLogger('Form'),
|
|
521
|
+
layout: createLogger('Layout'),
|
|
522
|
+
menu: createLogger('Menu'),
|
|
523
|
+
router: createLogger('Router')
|
|
524
|
+
};
|
|
525
|
+
|
|
471
526
|
/**
|
|
472
527
|
* ContentResolver provides a unified interface to fetch and render content
|
|
473
528
|
* using data providers, template resolvers, and optional caching.
|
|
@@ -506,10 +561,7 @@ function useBaseProps(props) {
|
|
|
506
561
|
*/
|
|
507
562
|
class TemplateResolver {
|
|
508
563
|
constructor(config) {
|
|
509
|
-
this.log =
|
|
510
|
-
namespace: 'ContentResolver',
|
|
511
|
-
enabled: config.enableLogging || false
|
|
512
|
-
});
|
|
564
|
+
this.log = createLogger('ContentResolver');
|
|
513
565
|
this.enableLogging = config.enableLogging || false;
|
|
514
566
|
this.templateResolver = config.templateResolver || new MustacheTemplateProvider();
|
|
515
567
|
if (config.cacheProvider === true) {
|
|
@@ -13121,6 +13173,22 @@ const defaultMarkdownRules = [
|
|
|
13121
13173
|
return null;
|
|
13122
13174
|
}
|
|
13123
13175
|
}];
|
|
13176
|
+
/**
|
|
13177
|
+
* Parse style string into React style object
|
|
13178
|
+
*/
|
|
13179
|
+
function parseStyleString(styleStr) {
|
|
13180
|
+
const styles = {};
|
|
13181
|
+
if (!styleStr) return styles;
|
|
13182
|
+
styleStr.split(';').forEach(declaration => {
|
|
13183
|
+
const [property, value] = declaration.split(':').map(s => s.trim());
|
|
13184
|
+
if (property && value) {
|
|
13185
|
+
// Convert kebab-case to camelCase
|
|
13186
|
+
const camelProperty = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
13187
|
+
styles[camelProperty] = value;
|
|
13188
|
+
}
|
|
13189
|
+
});
|
|
13190
|
+
return styles;
|
|
13191
|
+
}
|
|
13124
13192
|
/**
|
|
13125
13193
|
* Default fallback component - renders element as-is with SafeSpan content
|
|
13126
13194
|
*/
|
|
@@ -13128,6 +13196,9 @@ const defaultFallback = (element, key) => {
|
|
|
13128
13196
|
const tagName = element.tagName.toLowerCase();
|
|
13129
13197
|
// Check if this is a void element (self-closing)
|
|
13130
13198
|
const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
13199
|
+
// Handle style attribute separately to convert to object
|
|
13200
|
+
const styleAttr = element.getAttribute('style');
|
|
13201
|
+
const style = styleAttr ? parseStyleString(styleAttr) : undefined;
|
|
13131
13202
|
const props = {
|
|
13132
13203
|
key,
|
|
13133
13204
|
...(element.className ? {
|
|
@@ -13136,7 +13207,10 @@ const defaultFallback = (element, key) => {
|
|
|
13136
13207
|
...(element.id ? {
|
|
13137
13208
|
id: element.id
|
|
13138
13209
|
} : {}),
|
|
13139
|
-
...
|
|
13210
|
+
...(style ? {
|
|
13211
|
+
style
|
|
13212
|
+
} : {}),
|
|
13213
|
+
...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id', 'style'].includes(attr.name)).map(attr => [attr.name, attr.value]))
|
|
13140
13214
|
};
|
|
13141
13215
|
if (voidElements.has(tagName)) {
|
|
13142
13216
|
// Void elements can't have children
|
|
@@ -13181,6 +13255,9 @@ function transformElement(element, key, config = {
|
|
|
13181
13255
|
const children = Array.from(element.children);
|
|
13182
13256
|
const tagName = element.tagName.toLowerCase();
|
|
13183
13257
|
const transformedChildren = children.map((child, index) => transformElement(child, `${key}-${index}`, config));
|
|
13258
|
+
// Handle style attribute separately to convert to object
|
|
13259
|
+
const styleAttr = element.getAttribute('style');
|
|
13260
|
+
const style = styleAttr ? parseStyleString(styleAttr) : undefined;
|
|
13184
13261
|
return /*#__PURE__*/React.createElement(tagName, {
|
|
13185
13262
|
key,
|
|
13186
13263
|
...(element.className ? {
|
|
@@ -13189,12 +13266,39 @@ function transformElement(element, key, config = {
|
|
|
13189
13266
|
...(element.id ? {
|
|
13190
13267
|
id: element.id
|
|
13191
13268
|
} : {}),
|
|
13192
|
-
...
|
|
13269
|
+
...(style ? {
|
|
13270
|
+
style
|
|
13271
|
+
} : {}),
|
|
13272
|
+
...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id', 'style'].includes(attr.name)).map(attr => [attr.name, attr.value]))
|
|
13193
13273
|
}, ...transformedChildren);
|
|
13194
13274
|
}
|
|
13195
13275
|
// No transformation needed - use fallback
|
|
13196
13276
|
return fallbackComponent(element, key);
|
|
13197
13277
|
}
|
|
13278
|
+
/**
|
|
13279
|
+
* Clean up invalid DOM nesting that can cause React errors
|
|
13280
|
+
*/
|
|
13281
|
+
function cleanupInvalidNesting(html) {
|
|
13282
|
+
const parser = new DOMParser();
|
|
13283
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
13284
|
+
// Find paragraphs that contain block-level elements (invalid nesting)
|
|
13285
|
+
const paragraphs = doc.querySelectorAll('p');
|
|
13286
|
+
const blockElements = ['div', 'section', 'article', 'aside', 'header', 'footer', 'nav', 'main', 'figure', 'blockquote', 'pre', 'table', 'form', 'fieldset', 'address'];
|
|
13287
|
+
paragraphs.forEach(p => {
|
|
13288
|
+
const hasBlockChildren = Array.from(p.children).some(child => blockElements.includes(child.tagName.toLowerCase()));
|
|
13289
|
+
if (hasBlockChildren) {
|
|
13290
|
+
// Convert paragraph to div to allow block children
|
|
13291
|
+
const div = doc.createElement('div');
|
|
13292
|
+
div.innerHTML = p.innerHTML;
|
|
13293
|
+
// Copy attributes
|
|
13294
|
+
Array.from(p.attributes).forEach(attr => {
|
|
13295
|
+
div.setAttribute(attr.name, attr.value);
|
|
13296
|
+
});
|
|
13297
|
+
p.parentNode?.replaceChild(div, p);
|
|
13298
|
+
}
|
|
13299
|
+
});
|
|
13300
|
+
return doc.body.innerHTML;
|
|
13301
|
+
}
|
|
13198
13302
|
/**
|
|
13199
13303
|
* Transform HTML string to React components
|
|
13200
13304
|
*/
|
|
@@ -13202,8 +13306,10 @@ function transformHtmlToReact(html, config = {
|
|
|
13202
13306
|
rules: defaultArticleRules
|
|
13203
13307
|
}) {
|
|
13204
13308
|
if (!html.trim()) return [];
|
|
13309
|
+
// Clean up invalid DOM nesting first
|
|
13310
|
+
const cleanHtml = cleanupInvalidNesting(html);
|
|
13205
13311
|
const parser = new DOMParser();
|
|
13206
|
-
const doc = parser.parseFromString(
|
|
13312
|
+
const doc = parser.parseFromString(cleanHtml, 'text/html');
|
|
13207
13313
|
return Array.from(doc.body.children).map((element, index) => transformElement(element, index.toString(), config));
|
|
13208
13314
|
}
|
|
13209
13315
|
/**
|
|
@@ -20128,29 +20234,6 @@ const FormPage = ({
|
|
|
20128
20234
|
});
|
|
20129
20235
|
};
|
|
20130
20236
|
|
|
20131
|
-
/**
|
|
20132
|
-
* QwickApps React Framework - Logger Utility
|
|
20133
|
-
*
|
|
20134
|
-
* Re-exports from @qwickapps/logging with framework-specific loggers
|
|
20135
|
-
*
|
|
20136
|
-
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
20137
|
-
*/
|
|
20138
|
-
// Re-export everything from the logging package
|
|
20139
|
-
/**
|
|
20140
|
-
* Framework-specific loggers
|
|
20141
|
-
*/
|
|
20142
|
-
const loggers = {
|
|
20143
|
-
scaffold: createLogger('Scaffold'),
|
|
20144
|
-
navigation: createLogger('Navigation'),
|
|
20145
|
-
auth: createLogger('Auth'),
|
|
20146
|
-
theme: createLogger('Theme'),
|
|
20147
|
-
palette: createLogger('Palette'),
|
|
20148
|
-
form: createLogger('Form'),
|
|
20149
|
-
layout: createLogger('Layout'),
|
|
20150
|
-
menu: createLogger('Menu'),
|
|
20151
|
-
router: createLogger('Router')
|
|
20152
|
-
};
|
|
20153
|
-
|
|
20154
20237
|
const logger = loggers.scaffold;
|
|
20155
20238
|
// Material UI breakpoints
|
|
20156
20239
|
const BREAKPOINTS = {
|
|
@@ -24140,4 +24223,4 @@ var ActionType;
|
|
|
24140
24223
|
ActionType["CANCEL"] = "cancel";
|
|
24141
24224
|
})(ActionType || (ActionType = {}));
|
|
24142
24225
|
|
|
24143
|
-
export { AVAILABLE_PALETTES, ActionModel, ActionType, AllPalettes, Article, ArticleModel, BasePage, Button, CardListGrid, CardListGridModel, ChoiceInputField, ChoiceInputFieldModel, Code, CodeModel, Content, ContentModel, CoverImageHeader, CoverImageHeaderModel, DataProvider, DataProxy, DimensionsProvider, FeatureCard, FeatureCardActionModel, FeatureCardModel, FeatureGrid, FeatureGridModel, FeatureItemModel, Footer, FooterItemModel, FooterModel, FooterSectionModel, FormBlock, FormBlockModel, FormMethod, FormPage, GridCell, GridLayout, HeaderActionModel, HeroBlock, HeroBlockModel, Html, HtmlInputField, Logo, Markdown, MetadataItemModel, Page, PageBannerHeader, PageBannerHeaderModel, PaletteAutumn, PaletteCosmic, PaletteDefault, PaletteOcean, PaletteProvider, PaletteSpring, PaletteSwitcher, PaletteSwitcherModel, PaletteWinter, ProductCard, ProductCardActionModel, ProductCardModel, ProductModel, QWICKAPP_COMPONENT, QwickApp, QwickAppsLogo, SafeSpan, SafeSpanModel, Section, SectionModel, SelectInputField, T, TextField, TextInputField, TextInputFieldModel, ThemeProvider, ThemeSwitcher, ThemeSwitcherModel, applyCustomPalette, clearUserPalettePreference, clearUserThemePreference, createPaletteFromCurrentTheme, deleteCustomPalette, exportPalette, getCSSVariable, getComputedTheme, getCurrentPalette, getCurrentTheme, getCustomPalettes, getPaletteConfig, getPaletteName, getSystemTheme, getThemePerformanceStats, importPalette, initializePalette, initializeTheme, loadUserPalettePreference, loadUserThemePreference, logThemePerformanceStats, loggers, resetThemePerformanceStats, resolveDimension, resolveDimensions, resolveSpacing, resolveSpacingProps, saveCustomPalette, savePalettePreference, saveThemePreference, saveUserPalettePreference, saveUserThemePreference, setCSSVariable, setPalette, setTheme, t, useBaseProps, useData, useDataBinding, useDataContext, useDataProvider, useDimensions, usePalette, useQwickApp, useResolveTemplate, useSafeLocation, useSafeNavigate, useTemplate, useTheme };
|
|
24226
|
+
export { AVAILABLE_PALETTES, ActionModel, ActionType, AllPalettes, Article, ArticleModel, BasePage, Button, CardListGrid, CardListGridModel, ChoiceInputField, ChoiceInputFieldModel, Code, CodeModel, Content, ContentModel, CoverImageHeader, CoverImageHeaderModel, DataProvider, DataProxy, DimensionsProvider, FeatureCard, FeatureCardActionModel, FeatureCardModel, FeatureGrid, FeatureGridModel, FeatureItemModel, Footer, FooterItemModel, FooterModel, FooterSectionModel, FormBlock, FormBlockModel, FormMethod, FormPage, GridCell, GridLayout, HeaderActionModel, HeroBlock, HeroBlockModel, Html, HtmlInputField, Logo, Markdown, MetadataItemModel, Page, PageBannerHeader, PageBannerHeaderModel, PaletteAutumn, PaletteCosmic, PaletteDefault, PaletteOcean, PaletteProvider, PaletteSpring, PaletteSwitcher, PaletteSwitcherModel, PaletteWinter, ProductCard, ProductCardActionModel, ProductCardModel, ProductModel, QWICKAPP_COMPONENT, QwickApp, QwickAppsLogo, SafeSpan, SafeSpanModel, Section, SectionModel, SelectInputField, T, TextField, TextInputField, TextInputFieldModel, ThemeProvider, ThemeSwitcher, ThemeSwitcherModel, applyCustomPalette, clearUserPalettePreference, clearUserThemePreference, createLogger, createPaletteFromCurrentTheme, deleteCustomPalette, exportPalette, getCSSVariable, getComputedTheme, getCurrentPalette, getCurrentTheme, getCustomPalettes, getPaletteConfig, getPaletteName, getSystemTheme, getThemePerformanceStats, importPalette, initializePalette, initializeTheme, loadUserPalettePreference, loadUserThemePreference, logThemePerformanceStats, loggers, resetThemePerformanceStats, resolveDimension, resolveDimensions, resolveSpacing, resolveSpacingProps, saveCustomPalette, savePalettePreference, saveThemePreference, saveUserPalettePreference, saveUserThemePreference, setCSSVariable, setPalette, setTheme, t, useBaseProps, useData, useDataBinding, useDataContext, useDataProvider, useDimensions, usePalette, useQwickApp, useResolveTemplate, useSafeLocation, useSafeNavigate, useTemplate, useTheme };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var material = require('@mui/material');
|
|
5
5
|
var React = require('react');
|
|
6
|
-
var logging = require('@qwickapps/logging');
|
|
7
6
|
var schema = require('@qwickapps/schema');
|
|
8
7
|
var classValidator = require('class-validator');
|
|
9
8
|
var classTransformer = require('class-transformer');
|
|
@@ -469,6 +468,63 @@ function useBaseProps(props) {
|
|
|
469
468
|
};
|
|
470
469
|
}
|
|
471
470
|
|
|
471
|
+
/**
|
|
472
|
+
* QwickApps React Framework - Logger Utility
|
|
473
|
+
*
|
|
474
|
+
* Optional logging with fallback to console when @qwickapps/logging is not available
|
|
475
|
+
*
|
|
476
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
477
|
+
*/
|
|
478
|
+
/**
|
|
479
|
+
* Console-based logger fallback
|
|
480
|
+
*/
|
|
481
|
+
class ConsoleLogger {
|
|
482
|
+
constructor(name) {
|
|
483
|
+
this.name = name;
|
|
484
|
+
}
|
|
485
|
+
debug(message, ...args) {
|
|
486
|
+
{
|
|
487
|
+
console.debug(`[${this.name}] ${message}`, ...args);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
info(message, ...args) {
|
|
491
|
+
console.info(`[${this.name}] ${message}`, ...args);
|
|
492
|
+
}
|
|
493
|
+
warn(message, ...args) {
|
|
494
|
+
console.warn(`[${this.name}] ${message}`, ...args);
|
|
495
|
+
}
|
|
496
|
+
error(message, ...args) {
|
|
497
|
+
console.error(`[${this.name}] ${message}`, ...args);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Create logger with optional @qwickapps/logging dependency
|
|
502
|
+
*/
|
|
503
|
+
function createLogger(name) {
|
|
504
|
+
try {
|
|
505
|
+
// Try to use @qwickapps/logging if available
|
|
506
|
+
const logging = require('@qwickapps/logging');
|
|
507
|
+
return logging.createLogger(name);
|
|
508
|
+
} catch {
|
|
509
|
+
// Fallback to console-based logger
|
|
510
|
+
return new ConsoleLogger(name);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Framework-specific loggers
|
|
515
|
+
*/
|
|
516
|
+
const loggers = {
|
|
517
|
+
scaffold: createLogger('Scaffold'),
|
|
518
|
+
navigation: createLogger('Navigation'),
|
|
519
|
+
auth: createLogger('Auth'),
|
|
520
|
+
theme: createLogger('Theme'),
|
|
521
|
+
palette: createLogger('Palette'),
|
|
522
|
+
form: createLogger('Form'),
|
|
523
|
+
layout: createLogger('Layout'),
|
|
524
|
+
menu: createLogger('Menu'),
|
|
525
|
+
router: createLogger('Router')
|
|
526
|
+
};
|
|
527
|
+
|
|
472
528
|
/**
|
|
473
529
|
* ContentResolver provides a unified interface to fetch and render content
|
|
474
530
|
* using data providers, template resolvers, and optional caching.
|
|
@@ -507,10 +563,7 @@ function useBaseProps(props) {
|
|
|
507
563
|
*/
|
|
508
564
|
class TemplateResolver {
|
|
509
565
|
constructor(config) {
|
|
510
|
-
this.log =
|
|
511
|
-
namespace: 'ContentResolver',
|
|
512
|
-
enabled: config.enableLogging || false
|
|
513
|
-
});
|
|
566
|
+
this.log = createLogger('ContentResolver');
|
|
514
567
|
this.enableLogging = config.enableLogging || false;
|
|
515
568
|
this.templateResolver = config.templateResolver || new schema.MustacheTemplateProvider();
|
|
516
569
|
if (config.cacheProvider === true) {
|
|
@@ -13122,6 +13175,22 @@ const defaultMarkdownRules = [
|
|
|
13122
13175
|
return null;
|
|
13123
13176
|
}
|
|
13124
13177
|
}];
|
|
13178
|
+
/**
|
|
13179
|
+
* Parse style string into React style object
|
|
13180
|
+
*/
|
|
13181
|
+
function parseStyleString(styleStr) {
|
|
13182
|
+
const styles = {};
|
|
13183
|
+
if (!styleStr) return styles;
|
|
13184
|
+
styleStr.split(';').forEach(declaration => {
|
|
13185
|
+
const [property, value] = declaration.split(':').map(s => s.trim());
|
|
13186
|
+
if (property && value) {
|
|
13187
|
+
// Convert kebab-case to camelCase
|
|
13188
|
+
const camelProperty = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
13189
|
+
styles[camelProperty] = value;
|
|
13190
|
+
}
|
|
13191
|
+
});
|
|
13192
|
+
return styles;
|
|
13193
|
+
}
|
|
13125
13194
|
/**
|
|
13126
13195
|
* Default fallback component - renders element as-is with SafeSpan content
|
|
13127
13196
|
*/
|
|
@@ -13129,6 +13198,9 @@ const defaultFallback = (element, key) => {
|
|
|
13129
13198
|
const tagName = element.tagName.toLowerCase();
|
|
13130
13199
|
// Check if this is a void element (self-closing)
|
|
13131
13200
|
const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
13201
|
+
// Handle style attribute separately to convert to object
|
|
13202
|
+
const styleAttr = element.getAttribute('style');
|
|
13203
|
+
const style = styleAttr ? parseStyleString(styleAttr) : undefined;
|
|
13132
13204
|
const props = {
|
|
13133
13205
|
key,
|
|
13134
13206
|
...(element.className ? {
|
|
@@ -13137,7 +13209,10 @@ const defaultFallback = (element, key) => {
|
|
|
13137
13209
|
...(element.id ? {
|
|
13138
13210
|
id: element.id
|
|
13139
13211
|
} : {}),
|
|
13140
|
-
...
|
|
13212
|
+
...(style ? {
|
|
13213
|
+
style
|
|
13214
|
+
} : {}),
|
|
13215
|
+
...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id', 'style'].includes(attr.name)).map(attr => [attr.name, attr.value]))
|
|
13141
13216
|
};
|
|
13142
13217
|
if (voidElements.has(tagName)) {
|
|
13143
13218
|
// Void elements can't have children
|
|
@@ -13182,6 +13257,9 @@ function transformElement(element, key, config = {
|
|
|
13182
13257
|
const children = Array.from(element.children);
|
|
13183
13258
|
const tagName = element.tagName.toLowerCase();
|
|
13184
13259
|
const transformedChildren = children.map((child, index) => transformElement(child, `${key}-${index}`, config));
|
|
13260
|
+
// Handle style attribute separately to convert to object
|
|
13261
|
+
const styleAttr = element.getAttribute('style');
|
|
13262
|
+
const style = styleAttr ? parseStyleString(styleAttr) : undefined;
|
|
13185
13263
|
return /*#__PURE__*/React.createElement(tagName, {
|
|
13186
13264
|
key,
|
|
13187
13265
|
...(element.className ? {
|
|
@@ -13190,12 +13268,39 @@ function transformElement(element, key, config = {
|
|
|
13190
13268
|
...(element.id ? {
|
|
13191
13269
|
id: element.id
|
|
13192
13270
|
} : {}),
|
|
13193
|
-
...
|
|
13271
|
+
...(style ? {
|
|
13272
|
+
style
|
|
13273
|
+
} : {}),
|
|
13274
|
+
...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id', 'style'].includes(attr.name)).map(attr => [attr.name, attr.value]))
|
|
13194
13275
|
}, ...transformedChildren);
|
|
13195
13276
|
}
|
|
13196
13277
|
// No transformation needed - use fallback
|
|
13197
13278
|
return fallbackComponent(element, key);
|
|
13198
13279
|
}
|
|
13280
|
+
/**
|
|
13281
|
+
* Clean up invalid DOM nesting that can cause React errors
|
|
13282
|
+
*/
|
|
13283
|
+
function cleanupInvalidNesting(html) {
|
|
13284
|
+
const parser = new DOMParser();
|
|
13285
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
13286
|
+
// Find paragraphs that contain block-level elements (invalid nesting)
|
|
13287
|
+
const paragraphs = doc.querySelectorAll('p');
|
|
13288
|
+
const blockElements = ['div', 'section', 'article', 'aside', 'header', 'footer', 'nav', 'main', 'figure', 'blockquote', 'pre', 'table', 'form', 'fieldset', 'address'];
|
|
13289
|
+
paragraphs.forEach(p => {
|
|
13290
|
+
const hasBlockChildren = Array.from(p.children).some(child => blockElements.includes(child.tagName.toLowerCase()));
|
|
13291
|
+
if (hasBlockChildren) {
|
|
13292
|
+
// Convert paragraph to div to allow block children
|
|
13293
|
+
const div = doc.createElement('div');
|
|
13294
|
+
div.innerHTML = p.innerHTML;
|
|
13295
|
+
// Copy attributes
|
|
13296
|
+
Array.from(p.attributes).forEach(attr => {
|
|
13297
|
+
div.setAttribute(attr.name, attr.value);
|
|
13298
|
+
});
|
|
13299
|
+
p.parentNode?.replaceChild(div, p);
|
|
13300
|
+
}
|
|
13301
|
+
});
|
|
13302
|
+
return doc.body.innerHTML;
|
|
13303
|
+
}
|
|
13199
13304
|
/**
|
|
13200
13305
|
* Transform HTML string to React components
|
|
13201
13306
|
*/
|
|
@@ -13203,8 +13308,10 @@ function transformHtmlToReact(html, config = {
|
|
|
13203
13308
|
rules: defaultArticleRules
|
|
13204
13309
|
}) {
|
|
13205
13310
|
if (!html.trim()) return [];
|
|
13311
|
+
// Clean up invalid DOM nesting first
|
|
13312
|
+
const cleanHtml = cleanupInvalidNesting(html);
|
|
13206
13313
|
const parser = new DOMParser();
|
|
13207
|
-
const doc = parser.parseFromString(
|
|
13314
|
+
const doc = parser.parseFromString(cleanHtml, 'text/html');
|
|
13208
13315
|
return Array.from(doc.body.children).map((element, index) => transformElement(element, index.toString(), config));
|
|
13209
13316
|
}
|
|
13210
13317
|
/**
|
|
@@ -20129,29 +20236,6 @@ const FormPage = ({
|
|
|
20129
20236
|
});
|
|
20130
20237
|
};
|
|
20131
20238
|
|
|
20132
|
-
/**
|
|
20133
|
-
* QwickApps React Framework - Logger Utility
|
|
20134
|
-
*
|
|
20135
|
-
* Re-exports from @qwickapps/logging with framework-specific loggers
|
|
20136
|
-
*
|
|
20137
|
-
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
20138
|
-
*/
|
|
20139
|
-
// Re-export everything from the logging package
|
|
20140
|
-
/**
|
|
20141
|
-
* Framework-specific loggers
|
|
20142
|
-
*/
|
|
20143
|
-
const loggers = {
|
|
20144
|
-
scaffold: logging.createLogger('Scaffold'),
|
|
20145
|
-
navigation: logging.createLogger('Navigation'),
|
|
20146
|
-
auth: logging.createLogger('Auth'),
|
|
20147
|
-
theme: logging.createLogger('Theme'),
|
|
20148
|
-
palette: logging.createLogger('Palette'),
|
|
20149
|
-
form: logging.createLogger('Form'),
|
|
20150
|
-
layout: logging.createLogger('Layout'),
|
|
20151
|
-
menu: logging.createLogger('Menu'),
|
|
20152
|
-
router: logging.createLogger('Router')
|
|
20153
|
-
};
|
|
20154
|
-
|
|
20155
20239
|
const logger = loggers.scaffold;
|
|
20156
20240
|
// Material UI breakpoints
|
|
20157
20241
|
const BREAKPOINTS = {
|
|
@@ -24191,6 +24275,7 @@ exports.ThemeSwitcher = ThemeSwitcher;
|
|
|
24191
24275
|
exports.applyCustomPalette = applyCustomPalette;
|
|
24192
24276
|
exports.clearUserPalettePreference = clearUserPalettePreference;
|
|
24193
24277
|
exports.clearUserThemePreference = clearUserThemePreference;
|
|
24278
|
+
exports.createLogger = createLogger;
|
|
24194
24279
|
exports.createPaletteFromCurrentTheme = createPaletteFromCurrentTheme;
|
|
24195
24280
|
exports.deleteCustomPalette = deleteCustomPalette;
|
|
24196
24281
|
exports.exportPalette = exportPalette;
|
|
@@ -24237,9 +24322,3 @@ exports.useSafeLocation = useSafeLocation;
|
|
|
24237
24322
|
exports.useSafeNavigate = useSafeNavigate;
|
|
24238
24323
|
exports.useTemplate = useTemplate;
|
|
24239
24324
|
exports.useTheme = useTheme;
|
|
24240
|
-
Object.keys(logging).forEach(function (k) {
|
|
24241
|
-
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
24242
|
-
enumerable: true,
|
|
24243
|
-
get: function () { return logging[k]; }
|
|
24244
|
-
});
|
|
24245
|
-
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplateResolver.d.ts","sourceRoot":"","sources":["../../src/templates/TemplateResolver.ts"],"names":[],"mappings":"AAOA,OAAO,EAAsB,YAAY,EAAsD,KAAK,EAA4B,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACzK,OAAO,EAEL,iBAAiB,EAEjB,sBAAsB,EACvB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACxD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,aAAa,CAAC,CAAwB;IAC9C,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,GAAG,CAAS;gBAER,MAAM,EAAE,sBAAsB;
|
|
1
|
+
{"version":3,"file":"TemplateResolver.d.ts","sourceRoot":"","sources":["../../src/templates/TemplateResolver.ts"],"names":[],"mappings":"AAOA,OAAO,EAAsB,YAAY,EAAsD,KAAK,EAA4B,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACzK,OAAO,EAEL,iBAAiB,EAEjB,sBAAsB,EACvB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACxD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,aAAa,CAAC,CAAwB;IAC9C,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,GAAG,CAAS;gBAER,MAAM,EAAE,sBAAsB;IAwBpC,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAI5D,MAAM,CAAC,CAAC,SAAS,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IAIlG;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAG,MAAM,CAAE;IAsB3D;;;OAGG;YACW,iBAAiB;CAsChC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlTransform.d.ts","sourceRoot":"","sources":["../../src/utils/htmlTransform.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;CAC/D;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;CACxE;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,EA0G9C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,EAqC/C,CAAC;
|
|
1
|
+
{"version":3,"file":"htmlTransform.d.ts","sourceRoot":"","sources":["../../src/utils/htmlTransform.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;CAC/D;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;CACxE;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,EA0G9C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,EAqC/C,CAAC;AAsBF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,EAAE,KAAK,MAAM,KAAG,KAAK,CAAC,SAoCrE,CAAC;AAkBF;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,eAAgD,GACvD,KAAK,CAAC,SAAS,CA0CjB;AAmCD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,eAAgD,GACvD,KAAK,CAAC,SAAS,EAAE,CAYnB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuB3D"}
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QwickApps React Framework - Logger Utility
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Optional logging with fallback to console when @qwickapps/logging is not available
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
8
|
+
export interface Logger {
|
|
9
|
+
debug(message: string, ...args: any[]): void;
|
|
10
|
+
info(message: string, ...args: any[]): void;
|
|
11
|
+
warn(message: string, ...args: any[]): void;
|
|
12
|
+
error(message: string, ...args: any[]): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create logger with optional @qwickapps/logging dependency
|
|
16
|
+
*/
|
|
17
|
+
declare function createLogger(name: string): Logger;
|
|
18
|
+
/**
|
|
19
|
+
* Re-export Logger type and createLogger function for compatibility
|
|
20
|
+
*/
|
|
21
|
+
export { createLogger };
|
|
10
22
|
/**
|
|
11
23
|
* Framework-specific loggers
|
|
12
24
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAqCD;;GAEG;AACH,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS1C;AAED;;GAEG;AACH,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAU1C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qwickapps/react-framework",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Complete React framework with responsive navigation, flexible layouts, theming system, and reusable components for building modern applications.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -48,12 +48,14 @@
|
|
|
48
48
|
"author": "QwickApps",
|
|
49
49
|
"license": "PolyForm-Shield-1.0.0",
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@qwickapps/logging": "^1.0.0",
|
|
52
51
|
"@qwickapps/schema": "^1.3.1",
|
|
53
52
|
"marked": "^4.3.0",
|
|
54
53
|
"md5": "^2.3.0",
|
|
55
54
|
"sanitize-html": "^2.17.0"
|
|
56
55
|
},
|
|
56
|
+
"optionalDependencies": {
|
|
57
|
+
"@qwickapps/logging": "^1.0.0"
|
|
58
|
+
},
|
|
57
59
|
"peerDependencies": {
|
|
58
60
|
"@emotion/react": ">=11.0.0",
|
|
59
61
|
"@emotion/styled": ">=11.0.0",
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { Box, Paper, Typography } from '@mui/material';
|
|
6
6
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
7
|
-
import { Button } from '
|
|
8
|
-
import { TextField } from '
|
|
9
|
-
import { GridCell } from '
|
|
10
|
-
import GridLayout from '
|
|
7
|
+
import { Button } from '../components/buttons/Button';
|
|
8
|
+
import { TextField } from '../components/input/TextField';
|
|
9
|
+
import { GridCell } from '../components/layout/GridCell';
|
|
10
|
+
import GridLayout from '../components/layout/GridLayout';
|
|
11
11
|
|
|
12
12
|
const meta: Meta = {
|
|
13
|
-
title: '
|
|
13
|
+
title: 'Forms/Enhanced Components',
|
|
14
14
|
parameters: {
|
|
15
15
|
layout: 'padded',
|
|
16
16
|
docs: {
|
|
@@ -43,14 +43,14 @@ export const BasicTextField: StoryObj = {
|
|
|
43
43
|
<TextField
|
|
44
44
|
label="Small Width"
|
|
45
45
|
width="small"
|
|
46
|
-
margin="
|
|
46
|
+
margin="normal"
|
|
47
47
|
padding="small"
|
|
48
48
|
/>
|
|
49
49
|
<br />
|
|
50
50
|
<TextField
|
|
51
51
|
label="Large Width"
|
|
52
52
|
width="large"
|
|
53
|
-
margin="
|
|
53
|
+
margin="normal"
|
|
54
54
|
padding="large"
|
|
55
55
|
background="primary.light"
|
|
56
56
|
/>
|
|
@@ -73,7 +73,7 @@ export const BasicButton: StoryObj = {
|
|
|
73
73
|
<Button
|
|
74
74
|
variant="contained"
|
|
75
75
|
width="large"
|
|
76
|
-
margin="
|
|
76
|
+
margin="normal"
|
|
77
77
|
padding="large"
|
|
78
78
|
>
|
|
79
79
|
Large Width
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
6
6
|
*/
|
|
7
|
-
import { Logger } from '
|
|
7
|
+
import { Logger, createLogger } from '../utils/logger';
|
|
8
8
|
import { CachedDataProvider, DataResponse, ICacheProvider, IDataProvider, MemoryCacheProvider, Model, MustacheTemplateProvider, SelectOptions } from '@qwickapps/schema';
|
|
9
9
|
import {
|
|
10
10
|
DataProxy,
|
|
@@ -51,11 +51,7 @@ export class TemplateResolver implements ITemplateResolver {
|
|
|
51
51
|
private log: Logger;
|
|
52
52
|
|
|
53
53
|
constructor(config: TemplateResolverConfig) {
|
|
54
|
-
this.log =
|
|
55
|
-
namespace: 'ContentResolver',
|
|
56
|
-
enabled: config.enableLogging || false
|
|
57
|
-
});
|
|
58
|
-
|
|
54
|
+
this.log = createLogger('ContentResolver');
|
|
59
55
|
this.enableLogging = config.enableLogging || false;
|
|
60
56
|
this.templateResolver = config.templateResolver || new MustacheTemplateProvider();
|
|
61
57
|
if (config.cacheProvider === true) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test for DOM nesting fix in HTML transformation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { transformHtmlToReact, defaultMarkdownRules } from '../htmlTransform';
|
|
6
|
+
|
|
7
|
+
describe('DOM nesting fix', () => {
|
|
8
|
+
it('should fix invalid paragraph nesting with block elements', () => {
|
|
9
|
+
// This is the problematic HTML that marked.js creates from mixed content
|
|
10
|
+
const invalidHtml = `
|
|
11
|
+
<p>You can mix HTML with Markdown when needed:</p>
|
|
12
|
+
<p><div style="padding: 16px; border: 1px solid #ccc;">
|
|
13
|
+
This is <strong>HTML with Markdown</strong> inside it!
|
|
14
|
+
</div></p>
|
|
15
|
+
<p>Back to regular content.</p>
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
// Should not throw React error
|
|
19
|
+
expect(() => {
|
|
20
|
+
const result = transformHtmlToReact(invalidHtml, { rules: defaultMarkdownRules });
|
|
21
|
+
expect(result).toBeDefined();
|
|
22
|
+
expect(Array.isArray(result)).toBe(true);
|
|
23
|
+
}).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should preserve valid nesting', () => {
|
|
27
|
+
const validHtml = `
|
|
28
|
+
<p>This is a valid paragraph with <strong>inline elements</strong>.</p>
|
|
29
|
+
<div>This is a valid div with block content.</div>
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const result = transformHtmlToReact(validHtml, { rules: defaultMarkdownRules });
|
|
33
|
+
expect(result).toBeDefined();
|
|
34
|
+
expect(result.length).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should handle deeply nested invalid structures', () => {
|
|
38
|
+
const deeplyNested = `
|
|
39
|
+
<p>
|
|
40
|
+
Outer paragraph
|
|
41
|
+
<div>
|
|
42
|
+
Block in paragraph
|
|
43
|
+
<p>Paragraph in div in paragraph</p>
|
|
44
|
+
</div>
|
|
45
|
+
</p>
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
expect(() => {
|
|
49
|
+
const result = transformHtmlToReact(deeplyNested, { rules: defaultMarkdownRules });
|
|
50
|
+
expect(result).toBeDefined();
|
|
51
|
+
}).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test for optional logging dependency fallback
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createLogger, loggers } from '../logger';
|
|
6
|
+
|
|
7
|
+
// Mock the require function to simulate missing @qwickapps/logging package
|
|
8
|
+
const originalRequire = require;
|
|
9
|
+
|
|
10
|
+
describe('Optional logging dependency', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Reset console spy before each test
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// Restore original require
|
|
18
|
+
(global as any).require = originalRequire;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should use console fallback when @qwickapps/logging is not available', () => {
|
|
22
|
+
// Mock require to throw an error (simulating missing package)
|
|
23
|
+
(global as any).require = jest.fn().mockImplementation((module: string) => {
|
|
24
|
+
if (module === '@qwickapps/logging') {
|
|
25
|
+
throw new Error('Cannot find module');
|
|
26
|
+
}
|
|
27
|
+
return originalRequire(module);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Spy on console methods
|
|
31
|
+
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
|
32
|
+
|
|
33
|
+
// Create a new logger (should use fallback)
|
|
34
|
+
const logger = createLogger('TestLogger');
|
|
35
|
+
logger.info('Test message');
|
|
36
|
+
|
|
37
|
+
expect(consoleSpy).toHaveBeenCalledWith('[TestLogger] Test message');
|
|
38
|
+
consoleSpy.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should have all framework loggers available', () => {
|
|
42
|
+
expect(loggers).toHaveProperty('scaffold');
|
|
43
|
+
expect(loggers).toHaveProperty('navigation');
|
|
44
|
+
expect(loggers).toHaveProperty('auth');
|
|
45
|
+
expect(loggers).toHaveProperty('theme');
|
|
46
|
+
expect(loggers).toHaveProperty('palette');
|
|
47
|
+
expect(loggers).toHaveProperty('form');
|
|
48
|
+
expect(loggers).toHaveProperty('layout');
|
|
49
|
+
expect(loggers).toHaveProperty('menu');
|
|
50
|
+
expect(loggers).toHaveProperty('router');
|
|
51
|
+
|
|
52
|
+
// Each logger should have the required methods
|
|
53
|
+
Object.values(loggers).forEach(logger => {
|
|
54
|
+
expect(typeof logger.debug).toBe('function');
|
|
55
|
+
expect(typeof logger.info).toBe('function');
|
|
56
|
+
expect(typeof logger.warn).toBe('function');
|
|
57
|
+
expect(typeof logger.error).toBe('function');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should suppress debug logs in production', () => {
|
|
62
|
+
const originalEnv = process.env.NODE_ENV;
|
|
63
|
+
process.env.NODE_ENV = 'production';
|
|
64
|
+
|
|
65
|
+
// Mock require to throw an error (use fallback)
|
|
66
|
+
(global as any).require = jest.fn().mockImplementation((module: string) => {
|
|
67
|
+
if (module === '@qwickapps/logging') {
|
|
68
|
+
throw new Error('Cannot find module');
|
|
69
|
+
}
|
|
70
|
+
return originalRequire(module);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const debugSpy = jest.spyOn(console, 'debug').mockImplementation();
|
|
74
|
+
|
|
75
|
+
const logger = createLogger('TestLogger');
|
|
76
|
+
logger.debug('Debug message');
|
|
77
|
+
|
|
78
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
79
|
+
|
|
80
|
+
debugSpy.mockRestore();
|
|
81
|
+
process.env.NODE_ENV = originalEnv;
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -179,6 +179,26 @@ export const defaultMarkdownRules: TransformRule[] = [
|
|
|
179
179
|
}
|
|
180
180
|
];
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Parse style string into React style object
|
|
184
|
+
*/
|
|
185
|
+
function parseStyleString(styleStr: string): React.CSSProperties {
|
|
186
|
+
const styles: React.CSSProperties = {};
|
|
187
|
+
|
|
188
|
+
if (!styleStr) return styles;
|
|
189
|
+
|
|
190
|
+
styleStr.split(';').forEach(declaration => {
|
|
191
|
+
const [property, value] = declaration.split(':').map(s => s.trim());
|
|
192
|
+
if (property && value) {
|
|
193
|
+
// Convert kebab-case to camelCase
|
|
194
|
+
const camelProperty = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
195
|
+
styles[camelProperty as keyof React.CSSProperties] = value;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return styles;
|
|
200
|
+
}
|
|
201
|
+
|
|
182
202
|
/**
|
|
183
203
|
* Default fallback component - renders element as-is with SafeSpan content
|
|
184
204
|
*/
|
|
@@ -191,13 +211,18 @@ export const defaultFallback = (element: Element, key: string): React.ReactNode
|
|
|
191
211
|
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
192
212
|
]);
|
|
193
213
|
|
|
214
|
+
// Handle style attribute separately to convert to object
|
|
215
|
+
const styleAttr = element.getAttribute('style');
|
|
216
|
+
const style = styleAttr ? parseStyleString(styleAttr) : undefined;
|
|
217
|
+
|
|
194
218
|
const props = {
|
|
195
219
|
key,
|
|
196
220
|
...(element.className ? { className: element.className } : {}),
|
|
197
221
|
...(element.id ? { id: element.id } : {}),
|
|
222
|
+
...(style ? { style } : {}),
|
|
198
223
|
...Object.fromEntries(
|
|
199
224
|
Array.from(element.attributes)
|
|
200
|
-
.filter(attr => !['class', 'id'].includes(attr.name))
|
|
225
|
+
.filter(attr => !['class', 'id', 'style'].includes(attr.name))
|
|
201
226
|
.map(attr => [attr.name, attr.value])
|
|
202
227
|
)
|
|
203
228
|
};
|
|
@@ -257,15 +282,20 @@ export function transformElement(
|
|
|
257
282
|
transformElement(child, `${key}-${index}`, config)
|
|
258
283
|
);
|
|
259
284
|
|
|
285
|
+
// Handle style attribute separately to convert to object
|
|
286
|
+
const styleAttr = element.getAttribute('style');
|
|
287
|
+
const style = styleAttr ? parseStyleString(styleAttr) : undefined;
|
|
288
|
+
|
|
260
289
|
return React.createElement(
|
|
261
290
|
tagName,
|
|
262
291
|
{
|
|
263
292
|
key,
|
|
264
293
|
...(element.className ? { className: element.className } : {}),
|
|
265
294
|
...(element.id ? { id: element.id } : {}),
|
|
295
|
+
...(style ? { style } : {}),
|
|
266
296
|
...Object.fromEntries(
|
|
267
297
|
Array.from(element.attributes)
|
|
268
|
-
.filter(attr => !['class', 'id'].includes(attr.name))
|
|
298
|
+
.filter(attr => !['class', 'id', 'style'].includes(attr.name))
|
|
269
299
|
.map(attr => [attr.name, attr.value])
|
|
270
300
|
)
|
|
271
301
|
},
|
|
@@ -277,6 +307,39 @@ export function transformElement(
|
|
|
277
307
|
return fallbackComponent(element, key);
|
|
278
308
|
}
|
|
279
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Clean up invalid DOM nesting that can cause React errors
|
|
312
|
+
*/
|
|
313
|
+
function cleanupInvalidNesting(html: string): string {
|
|
314
|
+
const parser = new DOMParser();
|
|
315
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
316
|
+
|
|
317
|
+
// Find paragraphs that contain block-level elements (invalid nesting)
|
|
318
|
+
const paragraphs = doc.querySelectorAll('p');
|
|
319
|
+
const blockElements = ['div', 'section', 'article', 'aside', 'header', 'footer', 'nav', 'main', 'figure', 'blockquote', 'pre', 'table', 'form', 'fieldset', 'address'];
|
|
320
|
+
|
|
321
|
+
paragraphs.forEach(p => {
|
|
322
|
+
const hasBlockChildren = Array.from(p.children).some(child =>
|
|
323
|
+
blockElements.includes(child.tagName.toLowerCase())
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (hasBlockChildren) {
|
|
327
|
+
// Convert paragraph to div to allow block children
|
|
328
|
+
const div = doc.createElement('div');
|
|
329
|
+
div.innerHTML = p.innerHTML;
|
|
330
|
+
|
|
331
|
+
// Copy attributes
|
|
332
|
+
Array.from(p.attributes).forEach(attr => {
|
|
333
|
+
div.setAttribute(attr.name, attr.value);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
p.parentNode?.replaceChild(div, p);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return doc.body.innerHTML;
|
|
341
|
+
}
|
|
342
|
+
|
|
280
343
|
/**
|
|
281
344
|
* Transform HTML string to React components
|
|
282
345
|
*/
|
|
@@ -286,8 +349,11 @@ export function transformHtmlToReact(
|
|
|
286
349
|
): React.ReactNode[] {
|
|
287
350
|
if (!html.trim()) return [];
|
|
288
351
|
|
|
352
|
+
// Clean up invalid DOM nesting first
|
|
353
|
+
const cleanHtml = cleanupInvalidNesting(html);
|
|
354
|
+
|
|
289
355
|
const parser = new DOMParser();
|
|
290
|
-
const doc = parser.parseFromString(
|
|
356
|
+
const doc = parser.parseFromString(cleanHtml, 'text/html');
|
|
291
357
|
|
|
292
358
|
return Array.from(doc.body.children).map((element, index) =>
|
|
293
359
|
transformElement(element, index.toString(), config)
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,16 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QwickApps React Framework - Logger Utility
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Optional logging with fallback to console when @qwickapps/logging is not available
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export interface Logger {
|
|
10
|
+
debug(message: string, ...args: any[]): void;
|
|
11
|
+
info(message: string, ...args: any[]): void;
|
|
12
|
+
warn(message: string, ...args: any[]): void;
|
|
13
|
+
error(message: string, ...args: any[]): void;
|
|
14
|
+
}
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Console-based logger fallback
|
|
18
|
+
*/
|
|
19
|
+
class ConsoleLogger implements Logger {
|
|
20
|
+
constructor(private name: string) {}
|
|
21
|
+
|
|
22
|
+
debug(message: string, ...args: any[]): void {
|
|
23
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
24
|
+
console.debug(`[${this.name}] ${message}`, ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
info(message: string, ...args: any[]): void {
|
|
29
|
+
console.info(`[${this.name}] ${message}`, ...args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
warn(message: string, ...args: any[]): void {
|
|
33
|
+
console.warn(`[${this.name}] ${message}`, ...args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
error(message: string, ...args: any[]): void {
|
|
37
|
+
console.error(`[${this.name}] ${message}`, ...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* No-op logger for when logging should be disabled
|
|
43
|
+
*/
|
|
44
|
+
class NoOpLogger implements Logger {
|
|
45
|
+
debug(): void {}
|
|
46
|
+
info(): void {}
|
|
47
|
+
warn(): void {}
|
|
48
|
+
error(): void {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create logger with optional @qwickapps/logging dependency
|
|
53
|
+
*/
|
|
54
|
+
function createLogger(name: string): Logger {
|
|
55
|
+
try {
|
|
56
|
+
// Try to use @qwickapps/logging if available
|
|
57
|
+
const logging = require('@qwickapps/logging');
|
|
58
|
+
return logging.createLogger(name);
|
|
59
|
+
} catch {
|
|
60
|
+
// Fallback to console-based logger
|
|
61
|
+
return new ConsoleLogger(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Re-export Logger type and createLogger function for compatibility
|
|
67
|
+
*/
|
|
68
|
+
export { createLogger };
|
|
14
69
|
|
|
15
70
|
/**
|
|
16
71
|
* Framework-specific loggers
|