@astrojs/markdown-remark 2.1.1 → 2.1.3
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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +18 -0
- package/dist/index.js +2 -2
- package/dist/rehype-images.d.ts +1 -1
- package/dist/rehype-images.js +5 -55
- package/dist/remark-collect-images.d.ts +2 -4
- package/dist/remark-collect-images.js +16 -17
- package/dist/types.d.ts +2 -5
- package/package.json +2 -2
- package/src/index.ts +2 -2
- package/src/rehype-images.ts +5 -67
- package/src/remark-collect-images.ts +20 -21
- package/src/types.ts +2 -5
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[35m@astrojs/markdown-remark:build: [0mcache hit, replaying output [
|
|
1
|
+
[35m@astrojs/markdown-remark:build: [0mcache hit, replaying output [2md62fcf6af591535d[0m
|
|
2
2
|
[35m@astrojs/markdown-remark:build: [0m
|
|
3
|
-
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@2.1.
|
|
3
|
+
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@2.1.3 build /home/runner/work/astro/astro/packages/markdown/remark
|
|
4
4
|
[35m@astrojs/markdown-remark:build: [0m> astro-scripts build "src/**/*.ts" && tsc -p tsconfig.json
|
|
5
5
|
[35m@astrojs/markdown-remark:build: [0m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @astrojs/markdown-remark
|
|
2
2
|
|
|
3
|
+
## 2.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#6744](https://github.com/withastro/astro/pull/6744) [`a1a4f45b5`](https://github.com/withastro/astro/commit/a1a4f45b51a80215fa7598da83bd0d9c5acd20d2) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix remote images in Markdown throwing errors when using `experimental.assets`
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`489dd8d69`](https://github.com/withastro/astro/commit/489dd8d69cdd9d7c243cf8bec96051a914984b9c), [`a1a4f45b5`](https://github.com/withastro/astro/commit/a1a4f45b51a80215fa7598da83bd0d9c5acd20d2), [`a1108e037`](https://github.com/withastro/astro/commit/a1108e037115cdb67d03505286c7d3a4fc2a1ff5), [`8b88e4cf1`](https://github.com/withastro/astro/commit/8b88e4cf15c8bea7942b3985380164e0edf7250b), [`d54cbe413`](https://github.com/withastro/astro/commit/d54cbe41349e55f8544212ad9320705f07325920), [`4c347ab51`](https://github.com/withastro/astro/commit/4c347ab51e46f2319d614f8577fe502e3dc816e2), [`ff0430786`](https://github.com/withastro/astro/commit/ff043078630e678348ae4f4757b3015b3b862c16), [`2f2e572e9`](https://github.com/withastro/astro/commit/2f2e572e937fd25451bbc78a05d55b7caa1ca3ec), [`7116c021a`](https://github.com/withastro/astro/commit/7116c021a39eac15a6e1264dfbd11bef0f5d618a)]:
|
|
10
|
+
- astro@2.2.0
|
|
11
|
+
|
|
12
|
+
## 2.1.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#6604](https://github.com/withastro/astro/pull/6604) [`7f7a8504b`](https://github.com/withastro/astro/commit/7f7a8504b5c2df4c99d3025931860c0d50992510) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix using optimized images in Markdown not working
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [[`7f7a8504b`](https://github.com/withastro/astro/commit/7f7a8504b5c2df4c99d3025931860c0d50992510), [`38e6ec21e`](https://github.com/withastro/astro/commit/38e6ec21e266ad8765d8ca2293034123b34e839a), [`f42f47dc6`](https://github.com/withastro/astro/commit/f42f47dc6a91cdb6534dab0ecbf9e8e85f00ba40)]:
|
|
19
|
+
- astro@2.1.5
|
|
20
|
+
|
|
3
21
|
## 2.1.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -69,7 +69,7 @@ async function renderMarkdown(content, opts) {
|
|
|
69
69
|
parser.use([remarkPrism(scopedClassName)]);
|
|
70
70
|
}
|
|
71
71
|
if (opts.experimentalAssets) {
|
|
72
|
-
parser.use([toRemarkCollectImages(
|
|
72
|
+
parser.use([toRemarkCollectImages()]);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
parser.use([
|
|
@@ -86,7 +86,7 @@ async function renderMarkdown(content, opts) {
|
|
|
86
86
|
parser.use([[plugin, pluginOpts]]);
|
|
87
87
|
});
|
|
88
88
|
if (opts.experimentalAssets) {
|
|
89
|
-
parser.use(rehypeImages(
|
|
89
|
+
parser.use(rehypeImages());
|
|
90
90
|
}
|
|
91
91
|
if (!isPerformanceBenchmark) {
|
|
92
92
|
parser.use([rehypeHeadingIds]);
|
package/dist/rehype-images.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { MarkdownVFile } from './types.js';
|
|
2
|
-
export declare function rehypeImages(
|
|
2
|
+
export declare function rehypeImages(): () => (tree: any, file: MarkdownVFile) => void;
|
package/dist/rehype-images.js
CHANGED
|
@@ -1,71 +1,21 @@
|
|
|
1
|
-
import { join as pathJoin } from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
1
|
import { visit } from "unist-util-visit";
|
|
4
|
-
|
|
5
|
-
function rehypeImages(imageService, assetsDir, getImageMetadata) {
|
|
2
|
+
function rehypeImages() {
|
|
6
3
|
return () => function(tree, file) {
|
|
7
4
|
visit(tree, (node) => {
|
|
8
|
-
var _a;
|
|
9
|
-
if (!assetsDir)
|
|
10
|
-
return;
|
|
5
|
+
var _a, _b;
|
|
11
6
|
if (node.type !== "element")
|
|
12
7
|
return;
|
|
13
8
|
if (node.tagName !== "img")
|
|
14
9
|
return;
|
|
15
10
|
if ((_a = node.properties) == null ? void 0 : _a.src) {
|
|
16
|
-
if (file.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let fileURL;
|
|
20
|
-
if (isAliasedPath(node.properties.src)) {
|
|
21
|
-
fileURL = new URL(stripAliasPath(node.properties.src), assetsDir);
|
|
22
|
-
} else {
|
|
23
|
-
fileURL = pathToFileURL(pathJoin(file.dirname, node.properties.src));
|
|
24
|
-
}
|
|
25
|
-
const fileData = getImageMetadata(fileURLToPath(fileURL));
|
|
26
|
-
fileURL.searchParams.append("origWidth", fileData.width.toString());
|
|
27
|
-
fileURL.searchParams.append("origHeight", fileData.height.toString());
|
|
28
|
-
fileURL.searchParams.append("origFormat", fileData.type.toString());
|
|
29
|
-
let options = {
|
|
30
|
-
src: {
|
|
31
|
-
src: fileURL,
|
|
32
|
-
width: fileData.width,
|
|
33
|
-
height: fileData.height,
|
|
34
|
-
format: fileData.type
|
|
35
|
-
},
|
|
36
|
-
alt: node.properties.alt
|
|
37
|
-
};
|
|
38
|
-
const validatedOptions = imageService.validateOptions ? imageService.validateOptions(options) : options;
|
|
39
|
-
const imageURL = imageService.getURL(validatedOptions);
|
|
40
|
-
node.properties = Object.assign(node.properties, {
|
|
41
|
-
src: imageURL,
|
|
42
|
-
...imageService.getHTMLAttributes !== void 0 ? imageService.getHTMLAttributes(validatedOptions) : {}
|
|
43
|
-
});
|
|
11
|
+
if ((_b = file.data.imagePaths) == null ? void 0 : _b.has(node.properties.src)) {
|
|
12
|
+
node.properties["__ASTRO_IMAGE_"] = node.properties.src;
|
|
13
|
+
delete node.properties.src;
|
|
44
14
|
}
|
|
45
15
|
}
|
|
46
16
|
});
|
|
47
17
|
};
|
|
48
18
|
}
|
|
49
|
-
function isAliasedPath(path) {
|
|
50
|
-
return path.startsWith("~/assets");
|
|
51
|
-
}
|
|
52
|
-
function stripAliasPath(path) {
|
|
53
|
-
return path.replace("~/assets/", "");
|
|
54
|
-
}
|
|
55
|
-
function isRelativePath(path) {
|
|
56
|
-
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
|
|
57
|
-
}
|
|
58
|
-
function startsWithDotDotSlash(path) {
|
|
59
|
-
const c1 = path[0];
|
|
60
|
-
const c2 = path[1];
|
|
61
|
-
const c3 = path[2];
|
|
62
|
-
return c1 === "." && c2 === "." && c3 === "/";
|
|
63
|
-
}
|
|
64
|
-
function startsWithDotSlash(path) {
|
|
65
|
-
const c1 = path[0];
|
|
66
|
-
const c2 = path[1];
|
|
67
|
-
return c1 === "." && c2 === "/";
|
|
68
|
-
}
|
|
69
19
|
export {
|
|
70
20
|
rehypeImages
|
|
71
21
|
};
|
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export default function toRemarkCollectImages(resolveImage: OptionalResolveImage): () => (tree: any, vfile: VFile) => Promise<void>;
|
|
4
|
-
export {};
|
|
1
|
+
import type { MarkdownVFile } from './types';
|
|
2
|
+
export default function toRemarkCollectImages(): () => (tree: any, vfile: MarkdownVFile) => Promise<void>;
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
|
-
function toRemarkCollectImages(
|
|
2
|
+
function toRemarkCollectImages() {
|
|
3
3
|
return () => async function(tree, vfile) {
|
|
4
4
|
if (typeof (vfile == null ? void 0 : vfile.path) !== "string")
|
|
5
5
|
return;
|
|
6
6
|
const imagePaths = /* @__PURE__ */ new Set();
|
|
7
|
-
visit(tree, "image",
|
|
8
|
-
|
|
7
|
+
visit(tree, "image", (node) => {
|
|
8
|
+
if (shouldOptimizeImage(node.url))
|
|
9
|
+
imagePaths.add(node.url);
|
|
9
10
|
});
|
|
10
|
-
|
|
11
|
-
vfile.data.imagePaths = [];
|
|
12
|
-
return;
|
|
13
|
-
} else if (resolveImage) {
|
|
14
|
-
const mapping = /* @__PURE__ */ new Map();
|
|
15
|
-
for (const path of Array.from(imagePaths)) {
|
|
16
|
-
const id = await resolveImage(path);
|
|
17
|
-
mapping.set(path, id);
|
|
18
|
-
}
|
|
19
|
-
visit(tree, "image", function raiseError(node) {
|
|
20
|
-
node.url = mapping.get(node.url);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
vfile.data.imagePaths = Array.from(imagePaths);
|
|
11
|
+
vfile.data.imagePaths = imagePaths;
|
|
24
12
|
};
|
|
25
13
|
}
|
|
14
|
+
function shouldOptimizeImage(src) {
|
|
15
|
+
return !isValidUrl(src) && !src.startsWith("/");
|
|
16
|
+
}
|
|
17
|
+
function isValidUrl(str) {
|
|
18
|
+
try {
|
|
19
|
+
new URL(str);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
26
25
|
export {
|
|
27
26
|
toRemarkCollectImages as default
|
|
28
27
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -47,10 +47,6 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
|
|
47
47
|
/** Used for frontmatter injection plugins */
|
|
48
48
|
frontmatter?: Record<string, any>;
|
|
49
49
|
experimentalAssets?: boolean;
|
|
50
|
-
imageService?: any;
|
|
51
|
-
assetsDir?: URL;
|
|
52
|
-
resolveImage?: (path: string) => Promise<string>;
|
|
53
|
-
getImageMetadata?: any;
|
|
54
50
|
}
|
|
55
51
|
export interface MarkdownHeading {
|
|
56
52
|
depth: number;
|
|
@@ -65,10 +61,11 @@ export interface MarkdownMetadata {
|
|
|
65
61
|
export interface MarkdownVFile extends VFile {
|
|
66
62
|
data: {
|
|
67
63
|
__astroHeadings?: MarkdownHeading[];
|
|
64
|
+
imagePaths?: Set<string>;
|
|
68
65
|
};
|
|
69
66
|
}
|
|
70
67
|
export interface MarkdownRenderingResult {
|
|
71
68
|
metadata: MarkdownMetadata;
|
|
72
|
-
vfile:
|
|
69
|
+
vfile: MarkdownVFile;
|
|
73
70
|
code: string;
|
|
74
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/markdown-remark",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "withastro",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"./dist/internal.js": "./dist/internal.js"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"astro": "^2.
|
|
20
|
+
"astro": "^2.2.0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@astrojs/prism": "^2.1.0",
|
package/src/index.ts
CHANGED
|
@@ -96,7 +96,7 @@ export async function renderMarkdown(
|
|
|
96
96
|
|
|
97
97
|
if (opts.experimentalAssets) {
|
|
98
98
|
// Apply later in case user plugins resolve relative image paths
|
|
99
|
-
parser.use([toRemarkCollectImages(
|
|
99
|
+
parser.use([toRemarkCollectImages()]);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -116,7 +116,7 @@ export async function renderMarkdown(
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
if (opts.experimentalAssets) {
|
|
119
|
-
parser.use(rehypeImages(
|
|
119
|
+
parser.use(rehypeImages());
|
|
120
120
|
}
|
|
121
121
|
if (!isPerformanceBenchmark) {
|
|
122
122
|
parser.use([rehypeHeadingIds]);
|
package/src/rehype-images.ts
CHANGED
|
@@ -1,81 +1,19 @@
|
|
|
1
|
-
import { join as pathJoin } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
1
|
import { visit } from 'unist-util-visit';
|
|
4
|
-
import {
|
|
5
|
-
import type { ImageMetadata, MarkdownVFile } from './types.js';
|
|
2
|
+
import type { MarkdownVFile } from './types.js';
|
|
6
3
|
|
|
7
|
-
export function rehypeImages(
|
|
4
|
+
export function rehypeImages() {
|
|
8
5
|
return () =>
|
|
9
6
|
function (tree: any, file: MarkdownVFile) {
|
|
10
7
|
visit(tree, (node) => {
|
|
11
|
-
if (!assetsDir) return;
|
|
12
8
|
if (node.type !== 'element') return;
|
|
13
9
|
if (node.tagName !== 'img') return;
|
|
14
10
|
|
|
15
11
|
if (node.properties?.src) {
|
|
16
|
-
if (file.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let fileURL: URL;
|
|
20
|
-
if (isAliasedPath(node.properties.src)) {
|
|
21
|
-
fileURL = new URL(stripAliasPath(node.properties.src), assetsDir);
|
|
22
|
-
} else {
|
|
23
|
-
fileURL = pathToFileURL(pathJoin(file.dirname, node.properties.src));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const fileData = getImageMetadata!(fileURLToPath(fileURL)) as ImageMetadata;
|
|
27
|
-
fileURL.searchParams.append('origWidth', fileData.width.toString());
|
|
28
|
-
fileURL.searchParams.append('origHeight', fileData.height.toString());
|
|
29
|
-
fileURL.searchParams.append('origFormat', fileData.type.toString());
|
|
30
|
-
|
|
31
|
-
let options = {
|
|
32
|
-
src: {
|
|
33
|
-
src: fileURL,
|
|
34
|
-
width: fileData.width,
|
|
35
|
-
height: fileData.height,
|
|
36
|
-
format: fileData.type,
|
|
37
|
-
},
|
|
38
|
-
alt: node.properties.alt,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const validatedOptions = imageService.validateOptions
|
|
42
|
-
? imageService.validateOptions(options)
|
|
43
|
-
: options;
|
|
44
|
-
|
|
45
|
-
const imageURL = imageService.getURL(validatedOptions);
|
|
46
|
-
node.properties = Object.assign(node.properties, {
|
|
47
|
-
src: imageURL,
|
|
48
|
-
...(imageService.getHTMLAttributes !== undefined
|
|
49
|
-
? imageService.getHTMLAttributes(validatedOptions)
|
|
50
|
-
: {}),
|
|
51
|
-
});
|
|
12
|
+
if (file.data.imagePaths?.has(node.properties.src)) {
|
|
13
|
+
node.properties['__ASTRO_IMAGE_'] = node.properties.src;
|
|
14
|
+
delete node.properties.src;
|
|
52
15
|
}
|
|
53
16
|
}
|
|
54
17
|
});
|
|
55
18
|
};
|
|
56
19
|
}
|
|
57
|
-
|
|
58
|
-
function isAliasedPath(path: string) {
|
|
59
|
-
return path.startsWith('~/assets');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function stripAliasPath(path: string) {
|
|
63
|
-
return path.replace('~/assets/', '');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function isRelativePath(path: string) {
|
|
67
|
-
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function startsWithDotDotSlash(path: string) {
|
|
71
|
-
const c1 = path[0];
|
|
72
|
-
const c2 = path[1];
|
|
73
|
-
const c3 = path[2];
|
|
74
|
-
return c1 === '.' && c2 === '.' && c3 === '/';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function startsWithDotSlash(path: string) {
|
|
78
|
-
const c1 = path[0];
|
|
79
|
-
const c2 = path[1];
|
|
80
|
-
return c1 === '.' && c2 === '/';
|
|
81
|
-
}
|
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
import type { Image } from 'mdast';
|
|
2
2
|
import { visit } from 'unist-util-visit';
|
|
3
|
-
import type {
|
|
3
|
+
import type { MarkdownVFile } from './types';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export default function toRemarkCollectImages(resolveImage: OptionalResolveImage) {
|
|
5
|
+
export default function toRemarkCollectImages() {
|
|
8
6
|
return () =>
|
|
9
|
-
async function (tree: any, vfile:
|
|
7
|
+
async function (tree: any, vfile: MarkdownVFile) {
|
|
10
8
|
if (typeof vfile?.path !== 'string') return;
|
|
11
9
|
|
|
12
10
|
const imagePaths = new Set<string>();
|
|
13
|
-
visit(tree, 'image',
|
|
14
|
-
imagePaths.add(node.url);
|
|
11
|
+
visit(tree, 'image', (node: Image) => {
|
|
12
|
+
if (shouldOptimizeImage(node.url)) imagePaths.add(node.url);
|
|
15
13
|
});
|
|
16
|
-
if (imagePaths.size === 0) {
|
|
17
|
-
vfile.data.imagePaths = [];
|
|
18
|
-
return;
|
|
19
|
-
} else if (resolveImage) {
|
|
20
|
-
const mapping = new Map<string, string>();
|
|
21
|
-
for (const path of Array.from(imagePaths)) {
|
|
22
|
-
const id = await resolveImage(path);
|
|
23
|
-
mapping.set(path, id);
|
|
24
|
-
}
|
|
25
|
-
visit(tree, 'image', function raiseError(node: Image) {
|
|
26
|
-
node.url = mapping.get(node.url)!;
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
14
|
|
|
30
|
-
vfile.data.imagePaths =
|
|
15
|
+
vfile.data.imagePaths = imagePaths;
|
|
31
16
|
};
|
|
32
17
|
}
|
|
18
|
+
|
|
19
|
+
function shouldOptimizeImage(src: string) {
|
|
20
|
+
// Optimize anything that is NOT external or an absolute path to `public/`
|
|
21
|
+
return !isValidUrl(src) && !src.startsWith('/');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isValidUrl(str: string): boolean {
|
|
25
|
+
try {
|
|
26
|
+
new URL(str);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -68,10 +68,6 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
|
|
68
68
|
/** Used for frontmatter injection plugins */
|
|
69
69
|
frontmatter?: Record<string, any>;
|
|
70
70
|
experimentalAssets?: boolean;
|
|
71
|
-
imageService?: any;
|
|
72
|
-
assetsDir?: URL;
|
|
73
|
-
resolveImage?: (path: string) => Promise<string>;
|
|
74
|
-
getImageMetadata?: any;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
export interface MarkdownHeading {
|
|
@@ -89,11 +85,12 @@ export interface MarkdownMetadata {
|
|
|
89
85
|
export interface MarkdownVFile extends VFile {
|
|
90
86
|
data: {
|
|
91
87
|
__astroHeadings?: MarkdownHeading[];
|
|
88
|
+
imagePaths?: Set<string>;
|
|
92
89
|
};
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
export interface MarkdownRenderingResult {
|
|
96
93
|
metadata: MarkdownMetadata;
|
|
97
|
-
vfile:
|
|
94
|
+
vfile: MarkdownVFile;
|
|
98
95
|
code: string;
|
|
99
96
|
}
|