music-metadata 11.12.3 → 11.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { IRatio } from '../type.js';
2
- export type StringEncoding = 'ascii' | 'utf8' | 'utf-16le' | 'ucs2' | 'base64url' | 'latin1' | 'hex';
2
+ export type StringEncoding = 'ascii' | 'utf8' | 'utf-16le' | 'utf-16be' | 'ucs2' | 'base64url' | 'latin1' | 'hex';
3
3
  export declare function getBit(buf: Uint8Array, off: number, bit: number): boolean;
4
4
  /**
5
5
  * Find delimiting zero in uint8Array
@@ -12,7 +12,7 @@ export function getBit(buf, off, bit) {
12
12
  */
13
13
  export function findZero(uint8Array, encoding) {
14
14
  const len = uint8Array.length;
15
- if (encoding === 'utf-16le') {
15
+ if (encoding === 'utf-16le' || encoding === 'utf-16be') {
16
16
  // Look for 0x00 0x00 on 2-byte boundary
17
17
  for (let i = 0; i + 1 < len; i += 2) {
18
18
  if (uint8Array[i] === 0 && uint8Array[i + 1] === 0)
@@ -57,6 +57,12 @@ export function decodeString(uint8Array, encoding) {
57
57
  throw new FieldDecodingError('Expected even number of octets for 16-bit unicode string');
58
58
  return decodeString(swapBytes(uint8Array), encoding);
59
59
  }
60
+ if (encoding === 'utf-16be') {
61
+ // There is no native UTF-16BE decoder; swap to UTF-16LE and decode that.
62
+ if ((uint8Array.length & 1) !== 0)
63
+ throw new FieldDecodingError('Expected even number of octets for 16-bit unicode string');
64
+ return new StringType(uint8Array.length, 'utf-16le').get(swapBytes(Uint8Array.from(uint8Array)), 0);
65
+ }
60
66
  return new StringType(uint8Array.length, encoding).get(uint8Array, 0);
61
67
  }
62
68
  export function stripNulls(str) {
@@ -95,8 +95,8 @@ export class ID3v1Parser extends BasicParser {
95
95
  debug('Skip checking for ID3v1 because the file-size is unknown');
96
96
  return;
97
97
  }
98
- if (this.apeHeader) {
99
- this.tokenizer.ignore(this.apeHeader.offset - this.tokenizer.position);
98
+ if (this.apeHeader && this.tokenizer.supportsRandomAccess()) {
99
+ this.tokenizer.setPosition(this.apeHeader.offset);
100
100
  const apeParser = new APEv2Parser(this.metadata, this.tokenizer, this.options);
101
101
  await apeParser.parseTags(this.apeHeader.footer);
102
102
  }
@@ -15,7 +15,27 @@ export function getFrameHeaderLength(majorVer) {
15
15
  default: throw makeUnexpectedMajorVersionError(majorVer);
16
16
  }
17
17
  }
18
- function readFrameFlags(b) {
18
+ function readFrameFlags(b, majorVer) {
19
+ // ID3v2.3 and ID3v2.4 use different bit layouts for the frame status and format flags.
20
+ if (majorVer === 3) {
21
+ // ID3v2.3: status `%abc00000`, format `%ijk00000` (https://id3.org/id3v2.3.0)
22
+ return {
23
+ status: {
24
+ tag_alter_preservation: util.getBit(b, 0, 7),
25
+ file_alter_preservation: util.getBit(b, 0, 6),
26
+ read_only: util.getBit(b, 0, 5)
27
+ },
28
+ format: {
29
+ compression: util.getBit(b, 1, 7),
30
+ encryption: util.getBit(b, 1, 6),
31
+ grouping_identity: util.getBit(b, 1, 5),
32
+ // Unsynchronisation and data-length-indicator do not exist as per-frame flags in ID3v2.3.
33
+ unsynchronisation: false,
34
+ data_length_indicator: false
35
+ }
36
+ };
37
+ }
38
+ // ID3v2.4: status `%0abc0000`, format `%0h00kmnp` (https://id3.org/id3v2.4.0-structure)
19
39
  return {
20
40
  status: {
21
41
  tag_alter_preservation: util.getBit(b, 0, 6),
@@ -23,7 +43,7 @@ function readFrameFlags(b) {
23
43
  read_only: util.getBit(b, 0, 4)
24
44
  },
25
45
  format: {
26
- grouping_identity: util.getBit(b, 1, 7),
46
+ grouping_identity: util.getBit(b, 1, 6),
27
47
  compression: util.getBit(b, 1, 3),
28
48
  encryption: util.getBit(b, 1, 2),
29
49
  unsynchronisation: util.getBit(b, 1, 1),
@@ -61,7 +81,7 @@ function parseFrameHeaderV23V24(uint8Array, majorVer, warningCollector) {
61
81
  const header = {
62
82
  id: textDecode(uint8Array.subarray(0, 4), 'ascii'),
63
83
  length: (majorVer === 4 ? UINT32SYNCSAFE : Token.UINT32_BE).get(uint8Array, 4),
64
- flags: readFrameFlags(uint8Array.subarray(8, 10))
84
+ flags: readFrameFlags(uint8Array.subarray(8, 10), majorVer)
65
85
  };
66
86
  if (!header.id.match(/^[A-Z0-9]{4}$/)) {
67
87
  warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`);
@@ -390,11 +390,11 @@ export class FrameParser {
390
390
  if (zeroIndex >= valueArray.length) {
391
391
  // No terminator found, decode full buffer remainder
392
392
  return {
393
- text: util.decodeString(valueArray, encoding.encoding),
393
+ text: util.decodeString(uint8Array, encoding.encoding),
394
394
  len: originalLen
395
395
  };
396
396
  }
397
- const txt = valueArray.subarray(0, zeroIndex);
397
+ const txt = uint8Array.subarray(0, bomSize + zeroIndex);
398
398
  return {
399
399
  text: util.decodeString(txt, encoding.encoding),
400
400
  len: bomSize + zeroIndex + FrameParser.getNullTerminatorLength(encoding.encoding)
@@ -109,7 +109,7 @@ export const TextEncodingToken = {
109
109
  case 0x01:
110
110
  return { encoding: 'utf-16le', bom: true };
111
111
  case 0x02:
112
- return { encoding: 'utf-16le', bom: false };
112
+ return { encoding: 'utf-16be', bom: false };
113
113
  case 0x03:
114
114
  return { encoding: 'utf8', bom: false };
115
115
  default:
@@ -3,6 +3,7 @@ import initDebug from 'debug';
3
3
  import { FourCcToken } from '../common/FourCC.js';
4
4
  import { makeUnexpectedFileContentError } from '../ParseError.js';
5
5
  import * as util from '../common/Util.js';
6
+ import { FieldDecodingError } from '../ParseError.js';
6
7
  const debug = initDebug('music-metadata:parser:MP4:atom');
7
8
  export class Mp4ContentError extends makeUnexpectedFileContentError('MP4') {
8
9
  }
@@ -79,6 +80,13 @@ const SecondsSinceMacEpoch = {
79
80
  return new Date(secondsSinceUnixEpoch * 1000);
80
81
  }
81
82
  };
83
+ const SecondsSinceMacEpoch64 = {
84
+ len: 8,
85
+ get: (buf, off) => {
86
+ const secondsSinceUnixEpoch = Number(Token.UINT64_BE.get(buf, off)) - 2082844800;
87
+ return new Date(secondsSinceUnixEpoch * 1000);
88
+ }
89
+ };
82
90
  /**
83
91
  * Token: Media Header Atom
84
92
  * Ref:
@@ -90,16 +98,35 @@ export class MdhdAtom extends FixedLengthAtom {
90
98
  super(len, 24, 'mdhd');
91
99
  }
92
100
  get(buf, off) {
93
- return {
94
- version: Token.UINT8.get(buf, off + 0),
95
- flags: Token.UINT24_BE.get(buf, off + 1),
96
- creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
97
- modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
98
- timeScale: Token.UINT32_BE.get(buf, off + 12),
99
- duration: Token.UINT32_BE.get(buf, off + 16),
100
- language: Token.UINT16_BE.get(buf, off + 20),
101
- quality: Token.UINT16_BE.get(buf, off + 22)
102
- };
101
+ const version = Token.UINT8.get(buf, off + 0);
102
+ const flags = Token.UINT24_BE.get(buf, off + 1);
103
+ switch (version) {
104
+ case 0:
105
+ // Version 0: 32-bit fields
106
+ return {
107
+ version,
108
+ flags,
109
+ creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
110
+ modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
111
+ timeScale: Token.UINT32_BE.get(buf, off + 12),
112
+ duration: Token.UINT32_BE.get(buf, off + 16),
113
+ language: Token.UINT16_BE.get(buf, off + 20),
114
+ quality: Token.UINT16_BE.get(buf, off + 22)
115
+ };
116
+ case 1:
117
+ return {
118
+ version,
119
+ flags,
120
+ creationTime: SecondsSinceMacEpoch64.get(buf, off + 4),
121
+ modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12),
122
+ timeScale: Token.UINT32_BE.get(buf, off + 20),
123
+ duration: Number(Token.UINT64_BE.get(buf, off + 24)),
124
+ language: Token.UINT16_BE.get(buf, off + 32),
125
+ quality: Token.UINT16_BE.get(buf, off + 34)
126
+ };
127
+ default:
128
+ throw new FieldDecodingError('Invalid mdhd version header');
129
+ }
103
130
  }
104
131
  }
105
132
  /**
@@ -110,16 +137,41 @@ export class MvhdAtom extends FixedLengthAtom {
110
137
  super(len, 100, 'mvhd');
111
138
  }
112
139
  get(buf, off) {
140
+ const version = Token.UINT8.get(buf, off);
141
+ const flags = Token.UINT24_BE.get(buf, off + 1);
142
+ if (version === 1) {
143
+ // Version 1: 64-bit creation/modification times and duration
144
+ return {
145
+ version,
146
+ flags,
147
+ creationTime: SecondsSinceMacEpoch64.get(buf, off + 4),
148
+ modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12),
149
+ timeScale: Token.UINT32_BE.get(buf, off + 20),
150
+ duration: Number(Token.UINT64_BE.get(buf, off + 24)),
151
+ preferredRate: Token.UINT32_BE.get(buf, off + 32),
152
+ preferredVolume: Token.UINT16_BE.get(buf, off + 36),
153
+ // ignore reserved: 10 bytes
154
+ // ignore matrix structure: 36 bytes
155
+ previewTime: Token.UINT32_BE.get(buf, off + 84),
156
+ previewDuration: Token.UINT32_BE.get(buf, off + 88),
157
+ posterTime: Token.UINT32_BE.get(buf, off + 92),
158
+ selectionTime: Token.UINT32_BE.get(buf, off + 96),
159
+ selectionDuration: Token.UINT32_BE.get(buf, off + 100),
160
+ currentTime: Token.UINT32_BE.get(buf, off + 104),
161
+ nextTrackID: Token.UINT32_BE.get(buf, off + 108)
162
+ };
163
+ }
164
+ // Version 0: 32-bit fields
113
165
  return {
114
- version: Token.UINT8.get(buf, off),
115
- flags: Token.UINT24_BE.get(buf, off + 1),
166
+ version,
167
+ flags,
116
168
  creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
117
169
  modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
118
170
  timeScale: Token.UINT32_BE.get(buf, off + 12),
119
171
  duration: Token.UINT32_BE.get(buf, off + 16),
120
172
  preferredRate: Token.UINT32_BE.get(buf, off + 20),
121
173
  preferredVolume: Token.UINT16_BE.get(buf, off + 24),
122
- // ignore reserver: 10 bytes
174
+ // ignore reserved: 10 bytes
123
175
  // ignore matrix structure: 36 bytes
124
176
  previewTime: Token.UINT32_BE.get(buf, off + 72),
125
177
  previewDuration: Token.UINT32_BE.get(buf, off + 76),
@@ -174,19 +226,44 @@ export class TrackHeaderAtom {
174
226
  this.len = len;
175
227
  }
176
228
  get(buf, off) {
177
- return {
178
- version: Token.UINT8.get(buf, off),
179
- flags: Token.UINT24_BE.get(buf, off + 1),
180
- creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
181
- modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
182
- trackId: Token.UINT32_BE.get(buf, off + 12),
183
- // reserved 4 bytes
184
- duration: Token.UINT32_BE.get(buf, off + 20),
185
- layer: Token.UINT16_BE.get(buf, off + 24),
186
- alternateGroup: Token.UINT16_BE.get(buf, off + 26),
187
- volume: Token.UINT16_BE.get(buf, off + 28) // ToDo: fixed point
188
- // ToDo: add remaining fields
189
- };
229
+ const version = Token.UINT8.get(buf, off);
230
+ const flags = Token.UINT24_BE.get(buf, off + 1);
231
+ switch (version) {
232
+ case 0:
233
+ // Version 0: 32-bit fields
234
+ return {
235
+ version,
236
+ flags,
237
+ creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
238
+ modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
239
+ trackId: Token.UINT32_BE.get(buf, off + 12),
240
+ // reserved 4 bytes
241
+ duration: Token.UINT32_BE.get(buf, off + 20),
242
+ // reserved 8 bytes
243
+ layer: Token.UINT16_BE.get(buf, off + 32),
244
+ alternateGroup: Token.UINT16_BE.get(buf, off + 34),
245
+ volume: Token.UINT16_BE.get(buf, off + 36) // ToDo: fixed point
246
+ // ToDo: add remaining fields
247
+ };
248
+ case 1:
249
+ // Version 1: 64-bit creation/modification times and duration
250
+ return {
251
+ version,
252
+ flags,
253
+ creationTime: SecondsSinceMacEpoch64.get(buf, off + 4),
254
+ modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12),
255
+ trackId: Token.UINT32_BE.get(buf, off + 20),
256
+ // reserved 4 bytes
257
+ duration: Number(Token.UINT64_BE.get(buf, off + 28)),
258
+ // reserved 8 bytes
259
+ layer: Token.UINT16_BE.get(buf, off + 44),
260
+ alternateGroup: Token.UINT16_BE.get(buf, off + 46),
261
+ volume: Token.UINT16_BE.get(buf, off + 48) // ToDo: fixed point
262
+ // ToDo: add remaining fields
263
+ };
264
+ default:
265
+ throw new FieldDecodingError('Invalid tkhd version header');
266
+ }
190
267
  }
191
268
  }
192
269
  /**
@@ -31,7 +31,7 @@ const MPEG4 = {
31
31
  * https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies
32
32
  */
33
33
  SamplingFrequencies: [
34
- 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, null, null, -1
34
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, null, null, null
35
35
  ]
36
36
  /**
37
37
  * Channel Configurations
@@ -21,6 +21,10 @@ export declare const WaveFormat: {
21
21
  PCM: number;
22
22
  ADPCM: number;
23
23
  IEEE_FLOAT: number;
24
+ ALAW: number;
25
+ MULAW: number;
26
+ DVI_ADPCM: number;
27
+ GSM610: number;
24
28
  MPEG_ADTS_AAC: number;
25
29
  MPEG_LOAS: number;
26
30
  RAW_AAC1: number;
@@ -31,12 +35,17 @@ export declare const WaveFormat: {
31
35
  DRM: number;
32
36
  DTS2: number;
33
37
  MPEG: number;
38
+ MPEGLAYER3: number;
34
39
  };
35
40
  export type WaveFormat = typeof WaveFormat[keyof typeof WaveFormat];
36
41
  export declare const WaveFormatNameMap: {
37
42
  [WaveFormat.PCM]: string;
38
43
  [WaveFormat.ADPCM]: string;
39
44
  [WaveFormat.IEEE_FLOAT]: string;
45
+ [WaveFormat.ALAW]: string;
46
+ [WaveFormat.MULAW]: string;
47
+ [WaveFormat.DVI_ADPCM]: string;
48
+ [WaveFormat.GSM610]: string;
40
49
  [WaveFormat.MPEG_ADTS_AAC]: string;
41
50
  [WaveFormat.MPEG_LOAS]: string;
42
51
  [WaveFormat.RAW_AAC1]: string;
@@ -47,6 +56,7 @@ export declare const WaveFormatNameMap: {
47
56
  [WaveFormat.DRM]: string;
48
57
  [WaveFormat.DTS2]: string;
49
58
  [WaveFormat.MPEG]: string;
59
+ [WaveFormat.MPEGLAYER3]: string;
50
60
  };
51
61
  /**
52
62
  * "fmt" sub-chunk describes the sound data's format
@@ -7,9 +7,13 @@ export class WaveContentError extends makeUnexpectedFileContentError('Wave') {
7
7
  */
8
8
  export const WaveFormat = {
9
9
  PCM: 0x0001,
10
- // MPEG-4 and AAC Audio Types
11
10
  ADPCM: 0x0002,
12
11
  IEEE_FLOAT: 0x0003,
12
+ ALAW: 0x0006,
13
+ MULAW: 0x0007,
14
+ DVI_ADPCM: 0x0011,
15
+ GSM610: 0x0031,
16
+ // MPEG-4 and AAC Audio Types
13
17
  MPEG_ADTS_AAC: 0x1600,
14
18
  MPEG_LOAS: 0x1602,
15
19
  RAW_AAC1: 0x00FF,
@@ -20,12 +24,17 @@ export const WaveFormat = {
20
24
  ESST_AC3: 0x0241,
21
25
  DRM: 0x0009,
22
26
  DTS2: 0x2001,
23
- MPEG: 0x0050
27
+ MPEG: 0x0050,
28
+ MPEGLAYER3: 0x0055
24
29
  };
25
30
  export const WaveFormatNameMap = {
26
31
  [WaveFormat.PCM]: 'PCM',
27
32
  [WaveFormat.ADPCM]: 'ADPCM',
28
33
  [WaveFormat.IEEE_FLOAT]: 'IEEE_FLOAT',
34
+ [WaveFormat.ALAW]: 'ALAW',
35
+ [WaveFormat.MULAW]: 'MULAW',
36
+ [WaveFormat.DVI_ADPCM]: 'DVI_ADPCM',
37
+ [WaveFormat.GSM610]: 'GSM610',
29
38
  [WaveFormat.MPEG_ADTS_AAC]: 'MPEG_ADTS_AAC',
30
39
  [WaveFormat.MPEG_LOAS]: 'MPEG_LOAS',
31
40
  [WaveFormat.RAW_AAC1]: 'RAW_AAC1',
@@ -35,7 +44,8 @@ export const WaveFormatNameMap = {
35
44
  [WaveFormat.ESST_AC3]: 'ESST_AC3',
36
45
  [WaveFormat.DRM]: 'DRM',
37
46
  [WaveFormat.DTS2]: 'DTS2',
38
- [WaveFormat.MPEG]: 'MPEG'
47
+ [WaveFormat.MPEG]: 'MPEG',
48
+ [WaveFormat.MPEGLAYER3]: 'MPEGLAYER3'
39
49
  };
40
50
  /**
41
51
  * format chunk; chunk-id is "fmt "
@@ -14,6 +14,7 @@ import { BasicParser } from '../common/BasicParser.js';
14
14
  export declare class WaveParser extends BasicParser {
15
15
  private fact;
16
16
  private blockAlign;
17
+ private avgBytesPerSec;
17
18
  private header;
18
19
  parse(): Promise<void>;
19
20
  parseRiffChunk(chunkSize: number): Promise<void>;
@@ -25,6 +25,7 @@ export class WaveParser extends BasicParser {
25
25
  constructor() {
26
26
  super(...arguments);
27
27
  this.blockAlign = 0;
28
+ this.avgBytesPerSec = 0;
28
29
  }
29
30
  async parse() {
30
31
  const riffHeader = await this.tokenizer.readToken(riff.Header);
@@ -76,8 +77,8 @@ export class WaveParser extends BasicParser {
76
77
  this.metadata.setFormat('bitsPerSample', fmt.wBitsPerSample);
77
78
  this.metadata.setFormat('sampleRate', fmt.nSamplesPerSec);
78
79
  this.metadata.setFormat('numberOfChannels', fmt.nChannels);
79
- this.metadata.setFormat('bitrate', fmt.nBlockAlign * fmt.nSamplesPerSec * 8);
80
80
  this.blockAlign = fmt.nBlockAlign;
81
+ this.avgBytesPerSec = fmt.nAvgBytesPerSec;
81
82
  break;
82
83
  }
83
84
  case 'id3 ': // The way Picard, FooBar currently stores, ID3 meta-data
@@ -106,11 +107,11 @@ export class WaveParser extends BasicParser {
106
107
  this.metadata.setFormat('duration', numberOfSamples / this.metadata.format.sampleRate);
107
108
  }
108
109
  }
109
- if (this.metadata.format.codec === 'ADPCM') { // ADPCM is 4 bits lossy encoding resulting in 352kbps
110
- this.metadata.setFormat('bitrate', 352000);
110
+ if (this.avgBytesPerSec > 0) {
111
+ this.metadata.setFormat('bitrate', this.avgBytesPerSec * 8);
111
112
  }
112
- else if (this.metadata.format.sampleRate) {
113
- this.metadata.setFormat('bitrate', this.blockAlign * this.metadata.format.sampleRate * 8);
113
+ else if (this.metadata.format.duration) {
114
+ this.metadata.setFormat('bitrate', chunkSize * 8 / this.metadata.format.duration);
114
115
  }
115
116
  await this.tokenizer.ignore(header.chunkSize);
116
117
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "music-metadata",
3
3
  "description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
4
- "version": "11.12.3",
4
+ "version": "11.14.0",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -114,30 +114,30 @@
114
114
  "dependencies": {
115
115
  "@borewit/text-codec": "^0.2.2",
116
116
  "@tokenizer/token": "^0.3.0",
117
- "content-type": "^1.0.5",
117
+ "content-type": "^2.0.0",
118
118
  "debug": "^4.4.3",
119
- "file-type": "^21.3.1",
120
- "media-typer": "^1.1.0",
121
- "strtok3": "^10.3.4",
119
+ "file-type": "^21.3.4",
120
+ "media-typer": "^2.0.0",
121
+ "strtok3": "^10.3.5",
122
122
  "token-types": "^6.1.2",
123
123
  "uint8array-extras": "^1.5.0",
124
124
  "win-guid": "^0.2.1"
125
125
  },
126
126
  "devDependencies": {
127
- "@biomejs/biome": "2.4.6",
127
+ "@biomejs/biome": "2.5.3",
128
128
  "@types/chai": "^5.2.3",
129
129
  "@types/chai-as-promised": "^8.0.2",
130
- "@types/content-type": "^1.1.9",
131
- "@types/debug": "^4.1.12",
130
+ "@types/content-type": "^2.0.0",
131
+ "@types/debug": "^4.1.13",
132
132
  "@types/media-typer": "^1.1.3",
133
133
  "@types/mocha": "^10.0.10",
134
- "@types/node": "^25.2.0",
134
+ "@types/node": "^26.1.1",
135
135
  "c8": "^11.0.0",
136
136
  "chai": "^6.2.2",
137
137
  "chai-as-promised": "^8.0.2",
138
138
  "del-cli": "^7.0.0",
139
139
  "mime": "^4.1.0",
140
- "mocha": "^11.7.5",
140
+ "mocha": "^11.7.6",
141
141
  "node-readable-to-web-readable-stream": "^0.4.2",
142
142
  "remark-cli": "^12.0.1",
143
143
  "remark-preset-lint-consistent": "^6.0.1",