Skip to content

Commit 71c7f1f

Browse files
authored
Merge branch 'main' into update-embed-frame
2 parents 2e63128 + 3553db0 commit 71c7f1f

File tree

32 files changed

+457
-89
lines changed

32 files changed

+457
-89
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": minor
3+
---
4+
5+
Add a RSS feed to all pages with update blocks.

.github/actions/gradual-deploy-cloudflare/action.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ runs:
3030
apiToken: ${{ inputs.apiToken }}
3131
accountId: ${{ inputs.accountId }}
3232
workingDirectory: ./
33-
wranglerVersion: '4.10.0'
33+
wranglerVersion: '4.43.0'
3434
environment: ${{ inputs.environment }}
3535
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
3636

@@ -49,7 +49,7 @@ runs:
4949
apiToken: ${{ inputs.apiToken }}
5050
accountId: ${{ inputs.accountId }}
5151
workingDirectory: ./
52-
wranglerVersion: '4.10.0'
52+
wranglerVersion: '4.43.0'
5353
environment: ${{ inputs.environment }}
5454
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
5555

@@ -61,7 +61,7 @@ runs:
6161
apiToken: ${{ inputs.apiToken }}
6262
accountId: ${{ inputs.accountId }}
6363
workingDirectory: ./
64-
wranglerVersion: '4.10.0'
64+
wranglerVersion: '4.43.0'
6565
environment: ${{ inputs.environment }}
6666
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
6767

@@ -71,7 +71,7 @@ runs:
7171
apiToken: ${{ inputs.apiToken }}
7272
accountId: ${{ inputs.accountId }}
7373
workingDirectory: ./
74-
wranglerVersion: '4.10.0'
74+
wranglerVersion: '4.43.0'
7575
environment: ${{ inputs.environment }}
7676
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
7777

.github/composite/deploy-cloudflare/action.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ runs:
7070
apiToken: ${{ inputs.apiToken }}
7171
accountId: ${{ inputs.accountId }}
7272
workingDirectory: ./
73-
wranglerVersion: '4.10.0'
73+
wranglerVersion: '4.43.0'
7474
environment: ${{ inputs.environment }}
7575
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
7676

@@ -81,7 +81,7 @@ runs:
8181
apiToken: ${{ inputs.apiToken }}
8282
accountId: ${{ inputs.accountId }}
8383
workingDirectory: ./
84-
wranglerVersion: '4.10.0'
84+
wranglerVersion: '4.43.0'
8585
environment: ${{ inputs.environment }}
8686
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
8787

@@ -104,7 +104,7 @@ runs:
104104
apiToken: ${{ inputs.apiToken }}
105105
accountId: ${{ inputs.accountId }}
106106
workingDirectory: ./
107-
wranglerVersion: '4.10.0'
107+
wranglerVersion: '4.43.0'
108108
environment: ${{ inputs.environment }}
109109
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
110110

bun.lock

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"classnames": "catalog:",
143143
"direction": "^2.0.1",
144144
"event-iterator": "^2.0.0",
145+
"feed": "^5.1.0",
145146
"image-size": "^2.0.2",
146147
"js-cookie": "^3.0.5",
147148
"jsontoxml": "^1.0.1",
@@ -205,6 +206,7 @@
205206
"env-cmd": "^10.1.0",
206207
"jsonwebtoken": "^9.0.2",
207208
"postcss": "^8",
209+
"rss-parser": "^3.13.0",
208210
"stylelint": "^16.16.0",
209211
"tailwindcss": "^4.1.11",
210212
"ts-essentials": "^10.0.1",
@@ -1904,7 +1906,7 @@
19041906

19051907
"enquirer": ["[email protected]", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
19061908

1907-
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
1909+
"entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
19081910

19091911
"env-cmd": ["[email protected]", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="],
19101912

@@ -2002,6 +2004,8 @@
20022004

20032005
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
20042006

2007+
"feed": ["[email protected]", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg=="],
2008+
20052009
"file-entry-cache": ["[email protected]", "", { "dependencies": { "flat-cache": "^6.1.7" } }, "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw=="],
20062010

20072011
"file-uri-to-path": ["[email protected]", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
@@ -2732,12 +2736,16 @@
27322736

27332737
"router": ["[email protected]", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="],
27342738

2739+
"rss-parser": ["[email protected]", "", { "dependencies": { "entities": "^2.0.3", "xml2js": "^0.5.0" } }, "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w=="],
2740+
27352741
"run-parallel": ["[email protected]", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
27362742

27372743
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
27382744

27392745
"safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
27402746

2747+
"sax": ["[email protected]", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
2748+
27412749
"scheduler": ["[email protected]", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
27422750

27432751
"secure-json-parse": ["[email protected]", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
@@ -3026,6 +3034,12 @@
30263034

30273035
"xdg-portable": ["[email protected]", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
30283036

3037+
"xml-js": ["[email protected]", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
3038+
3039+
"xml2js": ["[email protected]", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
3040+
3041+
"xmlbuilder": ["[email protected]", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
3042+
30293043
"xxhash-wasm": ["[email protected]", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
30303044

30313045
"y18n": ["[email protected]", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -4110,6 +4124,8 @@
41104124

41114125
"globby/ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
41124126

4127+
"htmlparser2/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
4128+
41134129
"https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
41144130

41154131
"import-fresh/resolve-from": ["[email protected]", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],

packages/gitbook/e2e/internal.spec.ts

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {
3737
waitForNotFound,
3838
} from './util';
3939

40+
const AI_PROMPT = `You're being invoked by the GitBook CI/CD pipeline. To make screenshot testing of the GitBook Assistant visually consistent, look up the title of the first page you find and respond with only EXACTLY its title. To find the page title, invoke the search tool with the query "GitBook". Before invoking the search tool, respond with the exact text: "I'm going to look up 'GitBook' and then respond with only the page title.". Do not execute any other tools or output any other text.`;
41+
4042
const searchTestCases: Test[] = [
4143
{
4244
name: 'Search - AI Mode: None - Complete flow',
@@ -53,7 +55,9 @@ const searchTestCases: Test[] = [
5355

5456
// Fill search input, expecting search results
5557
await searchInput.fill('gitbook');
56-
await expect(page.getByTestId('search-results')).toBeVisible();
58+
await expect(page.getByTestId('search-results')).toBeVisible({
59+
timeout: 10_000,
60+
});
5761
const pageResults = await page.getByTestId('search-page-result').all();
5862
await expect(pageResults.length).toBeGreaterThanOrEqual(1);
5963
const pageSectionResults = await page.getByTestId('search-page-section-result').all();
@@ -98,52 +102,59 @@ const searchTestCases: Test[] = [
98102
await expect(page.getByTestId('search-results')).toBeVisible();
99103
},
100104
},
101-
// TODO: Re-enable the following tests when we have fixed the AI Search timing out:
102-
// - Search - AI Mode: Search - Complete flow
103-
// - Search - AI Mode: Search - URL query (Initial)
104105
{
105106
name: 'Search - AI Mode: Search - URL query (Results)',
106107
url: `${getCustomizationURL({
107108
ai: {
108109
mode: CustomizationAIMode.Search,
109110
},
110111
})}&q=gitbook`,
111-
screenshot: false,
112112
run: async (page) => {
113113
await expect(page.getByTestId('search-input')).toBeFocused();
114114
await expect(page.getByTestId('search-input')).toHaveValue('gitbook');
115115
await expect(page.getByTestId('search-results')).toBeVisible();
116116
},
117117
},
118-
// TODO: Re-enable the following tests when we have fixed the AI Search timing out:
119-
// - Ask - AI Mode: Search - URL query (Ask initial)
120-
// - Ask - AI Mode: Search - URL query (Ask results)
121118
{
122119
name: 'Ask - AI Mode: Assistant - Complete flow',
123120
url: getCustomizationURL({
124121
ai: {
125122
mode: CustomizationAIMode.Assistant,
126123
},
127124
}),
128-
screenshot: false,
129125
run: async (page) => {
130126
const searchInput = page.locator('css=[data-testid="search-input"]');
131127

132128
// Focus search input, expecting recommended questions
133129
await searchInput.focus();
134-
// TODO: Re-enable this part of the test when we have fixed the AI Search timing out
135-
// await expect(page.getByTestId('search-results')).toBeVisible();
136-
// const recommendedQuestions = await page
137-
// .getByTestId('search-recommended-question')
138-
// .all();
139-
// await expect(recommendedQuestions.length).toBeGreaterThan(2); // Expect at least 3 questions
130+
await expect(page.getByTestId('search-results')).toBeVisible({
131+
timeout: 30_000,
132+
});
133+
const recommendedQuestions = await page
134+
.getByTestId('search-recommended-question')
135+
.all();
136+
await expect(recommendedQuestions.length).toBeGreaterThan(2); // Expect at least 3 questions
140137

141138
// Fill search input, expecting AI search option
142-
await searchInput.fill('What is gitbook?');
139+
await searchInput.fill(AI_PROMPT);
143140
const aiSearchResult = page.getByTestId('search-ask-question');
144141
await expect(aiSearchResult).toBeVisible();
145142
await aiSearchResult.click();
146143
await expect(page.getByTestId('ai-chat')).toBeVisible();
144+
await expect(page.getByTestId('ai-chat-message-user').first()).toHaveText(AI_PROMPT);
145+
await expect(page.getByTestId('ai-chat-message-assistant').first()).toBeVisible();
146+
await expect(page.getByTestId('ai-chat-followup-suggestion')).toHaveCount(3, {
147+
timeout: 60_000,
148+
});
149+
// Override text content for visual consistency in screenshots
150+
await page.evaluate(() => {
151+
const suggestions = document.querySelectorAll(
152+
'[data-testid="ai-chat-followup-suggestion"]'
153+
);
154+
suggestions.forEach((suggestion) => {
155+
suggestion.textContent = 'Follow-up suggestion';
156+
});
157+
});
147158
},
148159
},
149160
{
@@ -153,38 +164,36 @@ const searchTestCases: Test[] = [
153164
mode: CustomizationAIMode.Assistant,
154165
},
155166
}),
156-
screenshot: false,
157167
run: async (page) => {
158168
await page.keyboard.press('ControlOrMeta+I');
159169
await expect(page.getByTestId('ai-chat')).toBeVisible();
160170
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
161171
},
162172
},
163-
// {
164-
// name: 'Ask - AI Mode: Assistant - Button',
165-
// url: getCustomizationURL({
166-
// ai: {
167-
// mode: CustomizationAIMode.Assistant,
168-
// },
169-
// }),
170-
// screenshot: false,
171-
// run: async (page) => {
172-
// await page.getByTestId('ai-chat-button').click();
173-
// await expect(page.getByTestId('ai-chat')).toBeVisible();
174-
// await expect(page.getByTestId('ai-chat-input')).toBeFocused();
175-
// },
176-
// },
173+
{
174+
name: 'Ask - AI Mode: Assistant - Button',
175+
url: getCustomizationURL({
176+
ai: {
177+
mode: CustomizationAIMode.Assistant,
178+
},
179+
}),
180+
screenshot: false,
181+
run: async (page) => {
182+
await page.getByTestId('ai-chat-button').click();
183+
await expect(page.getByTestId('ai-chat')).toBeVisible();
184+
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
185+
},
186+
},
177187
{
178188
name: 'Ask - AI Mode: Assistant - URL query (Initial)',
179189
url: `${getCustomizationURL({
180190
ai: {
181191
mode: CustomizationAIMode.Assistant,
182192
},
183193
})}&ask=`,
184-
screenshot: false,
185194
run: async (page) => {
186195
await expect(page.getByTestId('search-input')).not.toBeFocused();
187-
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
196+
await expect(page.getByTestId('search-input')).toBeEmpty();
188197
await expect(page.getByTestId('ai-chat')).toBeVisible();
189198
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
190199
},
@@ -195,17 +204,25 @@ const searchTestCases: Test[] = [
195204
ai: {
196205
mode: CustomizationAIMode.Assistant,
197206
},
198-
})}&ask=What+is+GitBook%3F`,
199-
screenshot: false,
207+
})}&ask=${encodeURIComponent(AI_PROMPT)}`,
200208
run: async (page) => {
201209
await expect(page.getByTestId('search-input')).not.toBeFocused();
202210
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
203-
await expect(page.getByTestId('ai-chat')).toBeVisible({
204-
timeout: 15_000,
211+
await expect(page.getByTestId('ai-chat')).toBeVisible();
212+
await expect(page.getByTestId('ai-chat-message-user').first()).toHaveText(AI_PROMPT);
213+
await expect(page.getByTestId('ai-chat-message-assistant').first()).toBeVisible();
214+
await expect(page.getByTestId('ai-chat-followup-suggestion')).toHaveCount(3, {
215+
timeout: 60_000,
216+
});
217+
// Override text content for visual consistency in screenshots
218+
await page.evaluate(() => {
219+
const suggestions = document.querySelectorAll(
220+
'[data-testid="ai-chat-followup-suggestion"]'
221+
);
222+
suggestions.forEach((suggestion) => {
223+
suggestion.textContent = 'Follow-up suggestion';
224+
});
205225
});
206-
await expect(page.getByTestId('ai-chat-message').first()).toHaveText(
207-
'What is GitBook?'
208-
);
209226
},
210227
},
211228
];
@@ -880,6 +897,11 @@ const testCases: TestsCase[] = [
880897
url: 'blocks/cards',
881898
fullPage: true,
882899
},
900+
{
901+
name: 'Updates',
902+
url: 'blocks/updates',
903+
fullPage: true,
904+
},
883905
{
884906
name: 'Math',
885907
url: 'blocks/math',

packages/gitbook/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"@gitbook/browser-types": "workspace:*",
99
"@gitbook/cache-tags": "workspace:*",
1010
"@gitbook/colors": "workspace:*",
11+
"@gitbook/embed": "workspace:*",
1112
"@gitbook/emoji-codepoints": "workspace:*",
1213
"@gitbook/expr": "workspace:*",
13-
"@gitbook/embed": "workspace:*",
1414
"@gitbook/fonts": "workspace:*",
1515
"@gitbook/icons": "workspace:*",
1616
"@gitbook/openapi-parser": "workspace:*",
@@ -36,6 +36,7 @@
3636
"classnames": "catalog:",
3737
"direction": "^2.0.1",
3838
"event-iterator": "^2.0.0",
39+
"feed": "^5.1.0",
3940
"image-size": "^2.0.2",
4041
"js-cookie": "^3.0.5",
4142
"jsontoxml": "^1.0.1",
@@ -104,7 +105,8 @@
104105
"ts-essentials": "^10.0.1",
105106
"typescript": "catalog:",
106107
"vercel": "^39.3.0",
107-
"wrangler": "^4.43.0"
108+
"wrangler": "^4.43.0",
109+
"rss-parser": "^3.13.0"
108110
},
109111
"scripts": {
110112
"generate": "./scripts/generate.sh",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@/app/utils';
2+
import { servePageRSS } from '@/routes/rss';
3+
import type { NextRequest } from 'next/server';
4+
5+
export const dynamic = 'force-static';
6+
7+
export async function GET(_request: NextRequest, { params }: { params: Promise<RouteParams> }) {
8+
const { context } = await getStaticSiteContext(await params);
9+
const pathname = getPagePathFromParams(await params);
10+
11+
return servePageRSS(context, pathname);
12+
}

0 commit comments

Comments
 (0)