@duffcloudservices/cms 0.3.12 → 0.3.14
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 +332 -309
- package/dist/editor/editorBridge.js +127 -50
- package/dist/editor/editorBridge.js.map +1 -1
- package/dist/index.js +59 -13
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/package.json +90 -90
- package/src/components/DcsReviewShowcase.vue +321 -326
- package/src/components/PreviewRibbon.vue +612 -612
- package/src/components/ResponsiveImage.vue +55 -55
- package/src/composables/index.ts +10 -10
- package/src/composables/useMediaCarousel.ts +158 -158
- package/src/composables/useReleaseNotes.ts +153 -153
- package/src/composables/useResponsiveImage.ts +85 -85
- package/src/composables/useReviewContent.ts +150 -92
- package/src/composables/useSEO.ts +387 -387
- package/src/composables/useSiteVersion.ts +123 -123
- package/src/composables/useTextContent.ts +297 -297
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Convenience component that renders a responsive `<picture>` element for
|
|
4
|
-
* DCS CDN-hosted images. For non-CDN images it falls back to a plain `<img>`.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```vue
|
|
8
|
-
* <ResponsiveImage
|
|
9
|
-
* src="https://files.duffcloudservices.com/kept/assets/hero/abc-123.jpg"
|
|
10
|
-
* alt="Hero banner"
|
|
11
|
-
* context="hero"
|
|
12
|
-
* class="w-full h-full object-cover"
|
|
13
|
-
* />
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
import { useResponsiveImage } from '../composables/useResponsiveImage'
|
|
17
|
-
import type { ImageContext } from '@duffcloudservices/cms-core'
|
|
18
|
-
|
|
19
|
-
const props = defineProps<{
|
|
20
|
-
/** Image source URL (original CDN URL or local path). */
|
|
21
|
-
src: string
|
|
22
|
-
/** Alt text for accessibility. */
|
|
23
|
-
alt: string
|
|
24
|
-
/** Sizing context — determines which variants to include. */
|
|
25
|
-
context?: ImageContext
|
|
26
|
-
/** CSS class(es) applied to the `<img>` element. */
|
|
27
|
-
class?: string
|
|
28
|
-
/** Optional `sizes` attribute override. */
|
|
29
|
-
sizes?: string
|
|
30
|
-
/** Skip responsive variants and use the original URL only. */
|
|
31
|
-
original?: boolean
|
|
32
|
-
}>()
|
|
33
|
-
|
|
34
|
-
const image = useResponsiveImage({
|
|
35
|
-
src: () => props.src,
|
|
36
|
-
alt: () => props.alt,
|
|
37
|
-
context: () => props.context ?? 'content',
|
|
38
|
-
sizes: () => props.sizes,
|
|
39
|
-
original: () => props.original,
|
|
40
|
-
})
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<template>
|
|
44
|
-
<picture v-if="image.hasVariants && !original">
|
|
45
|
-
<source
|
|
46
|
-
v-for="source in image.sources"
|
|
47
|
-
:key="source.type"
|
|
48
|
-
:srcset="source.srcset"
|
|
49
|
-
:type="source.type"
|
|
50
|
-
:sizes="source.sizes"
|
|
51
|
-
/>
|
|
52
|
-
<img v-bind="image.imgProps" :class="props.class" />
|
|
53
|
-
</picture>
|
|
54
|
-
<img v-else v-bind="image.imgProps" :class="props.class" />
|
|
55
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Convenience component that renders a responsive `<picture>` element for
|
|
4
|
+
* DCS CDN-hosted images. For non-CDN images it falls back to a plain `<img>`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```vue
|
|
8
|
+
* <ResponsiveImage
|
|
9
|
+
* src="https://files.duffcloudservices.com/kept/assets/hero/abc-123.jpg"
|
|
10
|
+
* alt="Hero banner"
|
|
11
|
+
* context="hero"
|
|
12
|
+
* class="w-full h-full object-cover"
|
|
13
|
+
* />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { useResponsiveImage } from '../composables/useResponsiveImage'
|
|
17
|
+
import type { ImageContext } from '@duffcloudservices/cms-core'
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
/** Image source URL (original CDN URL or local path). */
|
|
21
|
+
src: string
|
|
22
|
+
/** Alt text for accessibility. */
|
|
23
|
+
alt: string
|
|
24
|
+
/** Sizing context — determines which variants to include. */
|
|
25
|
+
context?: ImageContext
|
|
26
|
+
/** CSS class(es) applied to the `<img>` element. */
|
|
27
|
+
class?: string
|
|
28
|
+
/** Optional `sizes` attribute override. */
|
|
29
|
+
sizes?: string
|
|
30
|
+
/** Skip responsive variants and use the original URL only. */
|
|
31
|
+
original?: boolean
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const image = useResponsiveImage({
|
|
35
|
+
src: () => props.src,
|
|
36
|
+
alt: () => props.alt,
|
|
37
|
+
context: () => props.context ?? 'content',
|
|
38
|
+
sizes: () => props.sizes,
|
|
39
|
+
original: () => props.original,
|
|
40
|
+
})
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<picture v-if="image.hasVariants && !original">
|
|
45
|
+
<source
|
|
46
|
+
v-for="source in image.sources"
|
|
47
|
+
:key="source.type"
|
|
48
|
+
:srcset="source.srcset"
|
|
49
|
+
:type="source.type"
|
|
50
|
+
:sizes="source.sizes"
|
|
51
|
+
/>
|
|
52
|
+
<img v-bind="image.imgProps" :class="props.class" />
|
|
53
|
+
</picture>
|
|
54
|
+
<img v-else v-bind="image.imgProps" :class="props.class" />
|
|
55
|
+
</template>
|
package/src/composables/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { useTextContent } from './useTextContent'
|
|
2
|
-
export { useSEO, createSiteSEO } from './useSEO'
|
|
3
|
-
export { useReleaseNotes } from './useReleaseNotes'
|
|
4
|
-
export { useSiteVersion } from './useSiteVersion'
|
|
5
|
-
export {
|
|
6
|
-
useReviewContent,
|
|
7
|
-
type ReviewItem,
|
|
8
|
-
type UseReviewContentConfig,
|
|
9
|
-
type UseReviewContentReturn,
|
|
10
|
-
} from './useReviewContent'
|
|
1
|
+
export { useTextContent } from './useTextContent'
|
|
2
|
+
export { useSEO, createSiteSEO } from './useSEO'
|
|
3
|
+
export { useReleaseNotes } from './useReleaseNotes'
|
|
4
|
+
export { useSiteVersion } from './useSiteVersion'
|
|
5
|
+
export {
|
|
6
|
+
useReviewContent,
|
|
7
|
+
type ReviewItem,
|
|
8
|
+
type UseReviewContentConfig,
|
|
9
|
+
type UseReviewContentReturn,
|
|
10
|
+
} from './useReviewContent'
|
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useMediaCarousel Composable
|
|
3
|
-
*
|
|
4
|
-
* Extracts media carousel items from text content keys following the pattern:
|
|
5
|
-
* `{prefix}.{N}.url`, `{prefix}.{N}.type`, `{prefix}.{N}.alt`
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```vue
|
|
9
|
-
* <script setup lang="ts">
|
|
10
|
-
* import { useTextContent, useMediaCarousel } from '@duffcloudservices/cms'
|
|
11
|
-
*
|
|
12
|
-
* const { t } = useTextContent({
|
|
13
|
-
* pageSlug: 'bio-mackenzie',
|
|
14
|
-
* defaults: {
|
|
15
|
-
* 'hero.media-carousel.0.url': '/images/staff/mackenzie.webp',
|
|
16
|
-
* 'hero.media-carousel.0.type': 'image',
|
|
17
|
-
* 'hero.media-carousel.0.alt': 'Mackenzie Kowalick',
|
|
18
|
-
* 'hero.media-carousel.1.url': '/videos/intro.mp4',
|
|
19
|
-
* 'hero.media-carousel.1.type': 'video',
|
|
20
|
-
* 'hero.media-carousel.1.alt': 'Introduction video',
|
|
21
|
-
* }
|
|
22
|
-
* })
|
|
23
|
-
*
|
|
24
|
-
* const { items } = useMediaCarousel({
|
|
25
|
-
* prefix: 'hero.media-carousel',
|
|
26
|
-
* t,
|
|
27
|
-
* defaults: [
|
|
28
|
-
* { url: '/images/staff/mackenzie.webp', type: 'image', alt: 'Mackenzie Kowalick' }
|
|
29
|
-
* ]
|
|
30
|
-
* })
|
|
31
|
-
* </script>
|
|
32
|
-
*
|
|
33
|
-
* <template>
|
|
34
|
-
* <MediaCarousel :items="items" />
|
|
35
|
-
* </template>
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import { computed, type ComputedRef } from 'vue'
|
|
40
|
-
import { isCdnAssetUrl } from '@duffcloudservices/cms-core'
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Media carousel item representing an image, video, or embed
|
|
44
|
-
*/
|
|
45
|
-
export interface MediaCarouselItem {
|
|
46
|
-
/** URL to the image, video file, or embed URL */
|
|
47
|
-
url: string
|
|
48
|
-
/** Type of media: 'image', 'video' (direct file), 'youtube', or 'instagram' */
|
|
49
|
-
type: 'image' | 'video' | 'youtube' | 'instagram'
|
|
50
|
-
/** Accessibility alt text */
|
|
51
|
-
alt?: string
|
|
52
|
-
/**
|
|
53
|
-
* Whether this image has responsive CDN variants available.
|
|
54
|
-
* Automatically set to `true` when the URL matches the DCS CDN asset pattern.
|
|
55
|
-
* Components rendering the carousel should use `<ResponsiveImage>` when this is `true`.
|
|
56
|
-
*/
|
|
57
|
-
responsive?: boolean
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Configuration for useMediaCarousel composable
|
|
62
|
-
*/
|
|
63
|
-
export interface UseMediaCarouselConfig {
|
|
64
|
-
/** Key prefix for carousel items (e.g., 'hero.media-carousel') */
|
|
65
|
-
prefix: string
|
|
66
|
-
/** The t() function from useTextContent */
|
|
67
|
-
t: (key: string, fallback?: string) => string
|
|
68
|
-
/** Default items to use if no content keys are found */
|
|
69
|
-
defaults?: MediaCarouselItem[]
|
|
70
|
-
/** Maximum number of items to look for (default: 10) */
|
|
71
|
-
maxItems?: number
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Return type for useMediaCarousel composable
|
|
76
|
-
*/
|
|
77
|
-
export interface UseMediaCarouselReturn {
|
|
78
|
-
/** Computed array of media carousel items */
|
|
79
|
-
items: ComputedRef<MediaCarouselItem[]>
|
|
80
|
-
/** Whether any items were found from content keys */
|
|
81
|
-
hasItems: ComputedRef<boolean>
|
|
82
|
-
/** Number of items in the carousel */
|
|
83
|
-
count: ComputedRef<number>
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Extract media carousel items from text content keys.
|
|
88
|
-
*
|
|
89
|
-
* Looks for keys in the format:
|
|
90
|
-
* - `{prefix}.{N}.url` - Required URL for the media
|
|
91
|
-
* - `{prefix}.{N}.type` - Type: 'image' or 'video' (defaults to 'image')
|
|
92
|
-
* - `{prefix}.{N}.alt` - Alt text for accessibility
|
|
93
|
-
*
|
|
94
|
-
* Items are sorted by index (0, 1, etc.) and only included if they have a valid URL.
|
|
95
|
-
*
|
|
96
|
-
* @param config - Configuration object
|
|
97
|
-
* @returns Media carousel helpers and state
|
|
98
|
-
*/
|
|
99
|
-
export function useMediaCarousel(config: UseMediaCarouselConfig): UseMediaCarouselReturn {
|
|
100
|
-
const {
|
|
101
|
-
prefix,
|
|
102
|
-
t,
|
|
103
|
-
defaults = [],
|
|
104
|
-
maxItems = 10,
|
|
105
|
-
} = config
|
|
106
|
-
|
|
107
|
-
const items = computed<MediaCarouselItem[]>(() => {
|
|
108
|
-
const result: MediaCarouselItem[] = []
|
|
109
|
-
|
|
110
|
-
// Look for items from 0 to maxItems
|
|
111
|
-
for (let i = 0; i < maxItems; i++) {
|
|
112
|
-
const urlKey = `${prefix}.${i}.url`
|
|
113
|
-
const typeKey = `${prefix}.${i}.type`
|
|
114
|
-
const altKey = `${prefix}.${i}.alt`
|
|
115
|
-
|
|
116
|
-
// Use a sentinel value to detect if the key exists
|
|
117
|
-
const url = t(urlKey, '')
|
|
118
|
-
|
|
119
|
-
// Skip if no URL (key doesn't exist or is empty)
|
|
120
|
-
if (!url || url === urlKey) {
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const typeValue = t(typeKey, 'image')
|
|
125
|
-
// Parse type value - support image, video, youtube, instagram
|
|
126
|
-
let type: 'image' | 'video' | 'youtube' | 'instagram' = 'image'
|
|
127
|
-
if (typeValue === 'video') type = 'video'
|
|
128
|
-
else if (typeValue === 'youtube') type = 'youtube'
|
|
129
|
-
else if (typeValue === 'instagram') type = 'instagram'
|
|
130
|
-
const alt = t(altKey, '')
|
|
131
|
-
|
|
132
|
-
result.push({
|
|
133
|
-
url,
|
|
134
|
-
type,
|
|
135
|
-
alt: alt && alt !== altKey ? alt : undefined,
|
|
136
|
-
// Flag CDN-hosted images as responsive so carousel components
|
|
137
|
-
// can render them with <ResponsiveImage> automatically
|
|
138
|
-
responsive: type === 'image' && isCdnAssetUrl(url),
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// If no items found from content keys, use defaults
|
|
143
|
-
if (result.length === 0) {
|
|
144
|
-
return defaults
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return result
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const hasItems = computed(() => items.value.length > 0)
|
|
151
|
-
const count = computed(() => items.value.length)
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
items,
|
|
155
|
-
hasItems,
|
|
156
|
-
count,
|
|
157
|
-
}
|
|
158
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* useMediaCarousel Composable
|
|
3
|
+
*
|
|
4
|
+
* Extracts media carousel items from text content keys following the pattern:
|
|
5
|
+
* `{prefix}.{N}.url`, `{prefix}.{N}.type`, `{prefix}.{N}.alt`
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```vue
|
|
9
|
+
* <script setup lang="ts">
|
|
10
|
+
* import { useTextContent, useMediaCarousel } from '@duffcloudservices/cms'
|
|
11
|
+
*
|
|
12
|
+
* const { t } = useTextContent({
|
|
13
|
+
* pageSlug: 'bio-mackenzie',
|
|
14
|
+
* defaults: {
|
|
15
|
+
* 'hero.media-carousel.0.url': '/images/staff/mackenzie.webp',
|
|
16
|
+
* 'hero.media-carousel.0.type': 'image',
|
|
17
|
+
* 'hero.media-carousel.0.alt': 'Mackenzie Kowalick',
|
|
18
|
+
* 'hero.media-carousel.1.url': '/videos/intro.mp4',
|
|
19
|
+
* 'hero.media-carousel.1.type': 'video',
|
|
20
|
+
* 'hero.media-carousel.1.alt': 'Introduction video',
|
|
21
|
+
* }
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* const { items } = useMediaCarousel({
|
|
25
|
+
* prefix: 'hero.media-carousel',
|
|
26
|
+
* t,
|
|
27
|
+
* defaults: [
|
|
28
|
+
* { url: '/images/staff/mackenzie.webp', type: 'image', alt: 'Mackenzie Kowalick' }
|
|
29
|
+
* ]
|
|
30
|
+
* })
|
|
31
|
+
* </script>
|
|
32
|
+
*
|
|
33
|
+
* <template>
|
|
34
|
+
* <MediaCarousel :items="items" />
|
|
35
|
+
* </template>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { computed, type ComputedRef } from 'vue'
|
|
40
|
+
import { isCdnAssetUrl } from '@duffcloudservices/cms-core'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Media carousel item representing an image, video, or embed
|
|
44
|
+
*/
|
|
45
|
+
export interface MediaCarouselItem {
|
|
46
|
+
/** URL to the image, video file, or embed URL */
|
|
47
|
+
url: string
|
|
48
|
+
/** Type of media: 'image', 'video' (direct file), 'youtube', or 'instagram' */
|
|
49
|
+
type: 'image' | 'video' | 'youtube' | 'instagram'
|
|
50
|
+
/** Accessibility alt text */
|
|
51
|
+
alt?: string
|
|
52
|
+
/**
|
|
53
|
+
* Whether this image has responsive CDN variants available.
|
|
54
|
+
* Automatically set to `true` when the URL matches the DCS CDN asset pattern.
|
|
55
|
+
* Components rendering the carousel should use `<ResponsiveImage>` when this is `true`.
|
|
56
|
+
*/
|
|
57
|
+
responsive?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Configuration for useMediaCarousel composable
|
|
62
|
+
*/
|
|
63
|
+
export interface UseMediaCarouselConfig {
|
|
64
|
+
/** Key prefix for carousel items (e.g., 'hero.media-carousel') */
|
|
65
|
+
prefix: string
|
|
66
|
+
/** The t() function from useTextContent */
|
|
67
|
+
t: (key: string, fallback?: string) => string
|
|
68
|
+
/** Default items to use if no content keys are found */
|
|
69
|
+
defaults?: MediaCarouselItem[]
|
|
70
|
+
/** Maximum number of items to look for (default: 10) */
|
|
71
|
+
maxItems?: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Return type for useMediaCarousel composable
|
|
76
|
+
*/
|
|
77
|
+
export interface UseMediaCarouselReturn {
|
|
78
|
+
/** Computed array of media carousel items */
|
|
79
|
+
items: ComputedRef<MediaCarouselItem[]>
|
|
80
|
+
/** Whether any items were found from content keys */
|
|
81
|
+
hasItems: ComputedRef<boolean>
|
|
82
|
+
/** Number of items in the carousel */
|
|
83
|
+
count: ComputedRef<number>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract media carousel items from text content keys.
|
|
88
|
+
*
|
|
89
|
+
* Looks for keys in the format:
|
|
90
|
+
* - `{prefix}.{N}.url` - Required URL for the media
|
|
91
|
+
* - `{prefix}.{N}.type` - Type: 'image' or 'video' (defaults to 'image')
|
|
92
|
+
* - `{prefix}.{N}.alt` - Alt text for accessibility
|
|
93
|
+
*
|
|
94
|
+
* Items are sorted by index (0, 1, etc.) and only included if they have a valid URL.
|
|
95
|
+
*
|
|
96
|
+
* @param config - Configuration object
|
|
97
|
+
* @returns Media carousel helpers and state
|
|
98
|
+
*/
|
|
99
|
+
export function useMediaCarousel(config: UseMediaCarouselConfig): UseMediaCarouselReturn {
|
|
100
|
+
const {
|
|
101
|
+
prefix,
|
|
102
|
+
t,
|
|
103
|
+
defaults = [],
|
|
104
|
+
maxItems = 10,
|
|
105
|
+
} = config
|
|
106
|
+
|
|
107
|
+
const items = computed<MediaCarouselItem[]>(() => {
|
|
108
|
+
const result: MediaCarouselItem[] = []
|
|
109
|
+
|
|
110
|
+
// Look for items from 0 to maxItems
|
|
111
|
+
for (let i = 0; i < maxItems; i++) {
|
|
112
|
+
const urlKey = `${prefix}.${i}.url`
|
|
113
|
+
const typeKey = `${prefix}.${i}.type`
|
|
114
|
+
const altKey = `${prefix}.${i}.alt`
|
|
115
|
+
|
|
116
|
+
// Use a sentinel value to detect if the key exists
|
|
117
|
+
const url = t(urlKey, '')
|
|
118
|
+
|
|
119
|
+
// Skip if no URL (key doesn't exist or is empty)
|
|
120
|
+
if (!url || url === urlKey) {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const typeValue = t(typeKey, 'image')
|
|
125
|
+
// Parse type value - support image, video, youtube, instagram
|
|
126
|
+
let type: 'image' | 'video' | 'youtube' | 'instagram' = 'image'
|
|
127
|
+
if (typeValue === 'video') type = 'video'
|
|
128
|
+
else if (typeValue === 'youtube') type = 'youtube'
|
|
129
|
+
else if (typeValue === 'instagram') type = 'instagram'
|
|
130
|
+
const alt = t(altKey, '')
|
|
131
|
+
|
|
132
|
+
result.push({
|
|
133
|
+
url,
|
|
134
|
+
type,
|
|
135
|
+
alt: alt && alt !== altKey ? alt : undefined,
|
|
136
|
+
// Flag CDN-hosted images as responsive so carousel components
|
|
137
|
+
// can render them with <ResponsiveImage> automatically
|
|
138
|
+
responsive: type === 'image' && isCdnAssetUrl(url),
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If no items found from content keys, use defaults
|
|
143
|
+
if (result.length === 0) {
|
|
144
|
+
return defaults
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const hasItems = computed(() => items.value.length > 0)
|
|
151
|
+
const count = computed(() => items.value.length)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
items,
|
|
155
|
+
hasItems,
|
|
156
|
+
count,
|
|
157
|
+
}
|
|
158
|
+
}
|