diff --git a/.eslintrc.js b/.eslintrc.js
index 358da1a..4ef67d3 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -25,6 +25,7 @@ module.exports = {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
+ 'import/prefer-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'no-restricted-syntax': 'off',
'no-continue': 'off',
@@ -36,5 +37,15 @@ module.exports = {
tsx: 'never',
},
],
+ 'prefer-destructuring': [
+ 'error',
+ {
+ array: false,
+ object: true,
+ },
+ {
+ enforceForRenamedProperties: false,
+ },
+ ],
},
};
diff --git a/.gitignore b/.gitignore
index f0981ac..2add8e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ dist/
# parcel
.cache/
+/src/unparse.ts
\ No newline at end of file
diff --git a/README.md b/README.md
index 7d58fc5..ddee4ea 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# clrc [](https://www.npmjs.com/package/clrc) [](https://github.com/mebtte/react-lrc/blob/master/LICENSE) [](https://bundlephobia.com/result?p=clrc)
-LRC format parser for JavaScript/TypeScript. Here is a [playground](https://mebtte.github.io/clrc).
+Parser for LRC and enhanced LRC. Here is a [playground](https://mebtte.github.io/clrc).
[2.x README](https://github.com/mebtte/clrc/blob/5c6efcbbfe08d4021e0a7d6252088c5deca428f7/README.md)
@@ -8,6 +8,7 @@ LRC format parser for JavaScript/TypeScript. Here is a [playground](https://mebt
- Typescript support
- Browser & Node.js support
+- Simple format and enhanced format support
## Install & Usage
@@ -65,13 +66,100 @@ The output is:
parse lrc string to array.
+### parseEnhanced(lrcString)
+
+parse enhanced lrc string to array. here is a example:
+
+```txt
+[ti: Somebody to Love]
+[00:00.00] <00:00.04> When <00:00.16> the <00:00.82> truth <00:01.29> is <00:01.63> found <00:03.09> to <00:03.37> be <00:05.92> lies
+```
+
+the output is:
+
+```json
+[
+ {
+ "lineNumber": 0,
+ "raw": "[ti: Somebody to Love]",
+ "type": "metadata",
+ "key": "ti",
+ "value": " Somebody to Love"
+ },
+ {
+ "lineNumber": 1,
+ "raw": "[00:00.00] <00:00.04> When <00:00.16> the <00:00.82> truth <00:01.29> is <00:01.63> found <00:03.09> to <00:03.37> be <00:05.92> lies ",
+ "type": "enhanced_lyric",
+ "startMillisecond": 0,
+ "content": " <00:00.04> When <00:00.16> the <00:00.82> truth <00:01.29> is <00:01.63> found <00:03.09> to <00:03.37> be <00:05.92> lies ",
+ "words": [
+ {
+ "index": 0,
+ "raw": "<00:00.04> When ",
+ "startMillisecond": 40,
+ "content": " When "
+ },
+ {
+ "index": 1,
+ "raw": "<00:00.16> the ",
+ "startMillisecond": 160,
+ "content": " the "
+ },
+ {
+ "index": 2,
+ "raw": "<00:00.82> truth ",
+ "startMillisecond": 820,
+ "content": " truth "
+ },
+ {
+ "index": 3,
+ "raw": "<00:01.29> is ",
+ "startMillisecond": 1290,
+ "content": " is "
+ },
+ {
+ "index": 4,
+ "raw": "<00:01.63> found ",
+ "startMillisecond": 1630,
+ "content": " found "
+ },
+ {
+ "index": 5,
+ "raw": "<00:03.09> to ",
+ "startMillisecond": 3090,
+ "content": " to "
+ },
+ {
+ "index": 6,
+ "raw": "<00:03.37> be ",
+ "startMillisecond": 3370,
+ "content": " be "
+ },
+ {
+ "index": 7,
+ "raw": "<00:05.92> lies ",
+ "startMillisecond": 5920,
+ "content": " lies "
+ }
+ ]
+ }
+]
+```
+
### LineType
types of line:
- `LineType.INVALID` means it's invalid line
-- `LineType.LYRIC` means it's lyric line
- `LineType.METADATA` means it's metadata line
+- `LineType.LYRIC` means it's lyric line
+- `LineType.ENHANCED_LYRIC` means it's enhanced lyric line
+
+## Contributors
+
+
+
+
## License
diff --git a/package.json b/package.json
index ad36114..abea49a 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dev": "tsc --declaration --watch",
"build": "tsc --declaration",
"lint-staged": "lint-staged",
- "prepare": "husky install",
+ "prepare": "husky install && npm run build",
"prepublish": "npm run build"
},
"repository": {
@@ -30,6 +30,7 @@
},
"homepage": "https://github.com/mebtte/clrc#readme",
"devDependencies": {
+ "@types/node": "^18.15.12",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"eslint": "^8.23.0",
diff --git a/playground/package.json b/playground/package.json
index 6cc1bc3..569cef7 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -6,13 +6,13 @@
"@types/react": "^18.0.18",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.26",
+ "clrc": "file:..",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"react-scripts": "5.0.1",
"styled-components": "^5.3.5",
- "typescript": "^4.8.2",
- "clrc": "file:.."
+ "typescript": "^4.8.2"
},
"scripts": {
"dev": "react-scripts start",
diff --git a/playground/src/app.tsx b/playground/src/app.tsx
index 802d469..0532650 100644
--- a/playground/src/app.tsx
+++ b/playground/src/app.tsx
@@ -1,9 +1,10 @@
-import React, { useState } from 'react';
+import React, { useDeferredValue, useEffect, useState } from 'react';
import styled from 'styled-components';
-import demoLrc from './lrc';
+import Github from './github';
import GlobalStyle from './global_style';
import JsonView from './json_view';
-import Github from './github';
+import { lrc as lrcDemo, enhancedLrc as enhancedLrcDemo } from './data';
+import Option from './option';
const Style = styled.div`
position: absolute;
@@ -13,10 +14,18 @@ const Style = styled.div`
left: 0;
display: flex;
+
+ > .editor {
+ flex: 1;
+ min-width: 0;
+
+ display: flex;
+ flex-direction: column;
+ }
`;
const Textarea = styled.textarea`
flex: 1;
- min-width: 0;
+ min-height: 0;
padding: 10px;
@@ -29,16 +38,26 @@ const Textarea = styled.textarea`
`;
const App = () => {
- const [lrc, setLrc] = useState(demoLrc);
+ const [enhanced, setEnhanced] = useState(false);
+
+ const [lrc, setLrc] = useState(enhanced ? enhancedLrcDemo : lrcDemo);
const onLrcChange = (event: React.ChangeEvent) =>
setLrc(event.target.value);
+ useEffect(() => {
+ setLrc(enhanced ? enhancedLrcDemo : lrcDemo);
+ }, [enhanced]);
+
+ const deferedLrc = useDeferredValue(lrc);
return (
<>
>
diff --git a/playground/src/lrc.ts b/playground/src/data.ts
similarity index 76%
rename from playground/src/lrc.ts
rename to playground/src/data.ts
index 5ba63bd..10c4792 100644
--- a/playground/src/lrc.ts
+++ b/playground/src/data.ts
@@ -1,4 +1,4 @@
-const lrc = `something wrong
+export const lrc = `something wrong
[by:mebtte]
[ar:Greyson Chance]
[01:17.62][00:51.80][01:43.27]Come with me for a little ride, see the shadows passing by
@@ -30,4 +30,10 @@ const lrc = `something wrong
[03:59.39]Come away with me, it's gonna be all right you'll see, you'll see
[04:24.46]Come away with me`;
-export default lrc;
+// from https://en.wikipedia.org/wiki/LRC_(file_format)#Enhanced_format|Enhanced%20LRC
+export const enhancedLrc = `[by: lrc-maker]
+[ti: Somebody to Love]
+
+[00:00.00] <00:00.04> When <00:00.16> the <00:00.82> truth <00:01.29> is <00:01.63> found <00:03.09> to <00:03.37> be <00:05.92> lies
+[00:06.47] <00:07.67> And <00:07.94> all <00:08.36> the <00:08.63> joy <00:10.28> within <00:10.53> you <00:13.09> dies
+[00:13.34] <00:14.32> Don't <00:14.73> you <00:15.14> want <00:15.57> somebody <00:16.09> to <00:16.46> love`;
diff --git a/playground/src/json_view.tsx b/playground/src/json_view.tsx
index 1ecf3a7..114560f 100644
--- a/playground/src/json_view.tsx
+++ b/playground/src/json_view.tsx
@@ -1,7 +1,7 @@
-import { useMemo, useDeferredValue } from 'react';
+import { useMemo } from 'react';
import styled from 'styled-components';
import JsonView from 'react-json-view';
-import { parse } from 'clrc';
+import { parse, parseEnhanced } from 'clrc';
const Style = styled.div`
padding: 10px;
@@ -13,9 +13,11 @@ const Style = styled.div`
background-color: rgb(222 222 222 / 0.2);
`;
-const Wrapper = ({ lrc }: { lrc: string }) => {
- const deferedLrc = useDeferredValue(lrc);
- const parsed = useMemo(() => parse(deferedLrc), [deferedLrc]);
+const Wrapper = ({ lrc, enhanced }: { lrc: string; enhanced: boolean }) => {
+ const parsed = useMemo(
+ () => (enhanced ? parseEnhanced(lrc) : parse(lrc)),
+ [lrc, enhanced]
+ );
return (
+ );
+}
+
+export default Option;
diff --git a/src/constants.ts b/src/constants.ts
index 70862a5..00736ef 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,8 +1,9 @@
// eslint-disable-next-line no-shadow
export enum LineType {
INVALID = 'invalid',
- LYRIC = 'lyric',
METADATA = 'metadata',
+ LYRIC = 'lyric',
+ ENHANCED_LYRIC = 'enhanced_lyric',
}
export interface Line {
@@ -26,3 +27,15 @@ export interface LyricLine extends Line {
export interface InvalidLine extends Line {
type: LineType.INVALID;
}
+
+export interface EnhancedWord {
+ index: number;
+ raw: string;
+ startMillisecond: number;
+ content: string;
+}
+
+export interface EnhancedLyricLine extends Omit {
+ type: LineType.ENHANCED_LYRIC;
+ words: EnhancedWord[];
+}
diff --git a/src/index.ts b/src/index.ts
index bfba188..dcedcf1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,21 @@
-import { LineType, Line, MetadataLine, LyricLine } from './constants';
+import {
+ LineType,
+ Line,
+ MetadataLine,
+ LyricLine,
+ EnhancedLyricLine,
+ EnhancedWord,
+} from './constants';
import parse from './parse';
+import parseEnhanced from './parse_enhanced';
-export { parse, LineType, Line, MetadataLine, LyricLine };
+export {
+ parse,
+ parseEnhanced,
+ LineType,
+ Line,
+ MetadataLine,
+ LyricLine,
+ EnhancedLyricLine,
+ EnhancedWord,
+};
diff --git a/src/parse.ts b/src/parse.ts
index d4b01f7..7a4a1b2 100644
--- a/src/parse.ts
+++ b/src/parse.ts
@@ -1,19 +1,16 @@
-import { LineType, Line, MetadataLine, LyricLine } from './constants';
+import { LineType, MetadataLine, LyricLine, InvalidLine } from './constants';
+import { timestampToMillsecond } from './utils';
-/**
- * allow multiple time tag
- * [time][time]content
- */
-const LYRIC_LINE = /^((?:\[\d+:\d+(?:\.\d+)?\])+)(.*)$/;
+const LYRIC_LINE = /^((?:\[\d{2,}:\d{2}(?:\.\d{2,3})?\])+)(.*)$/; // [time]content | [time][time][...]content
const METADATA_LINE = /^\[(.+?):(.*?)\]$/; // [key:value]
-const LYRIC_TIME = /^(\d+):(\d+)(?:\.(\d+))?$/; // 00:00.00 or 00:00
/**
* parse lrc string
* @author mebtte
*/
function parse(lrc: string) {
- const parsedLines: Line[] = [];
+ const parsedLines: (InvalidLine | MetadataLine | LyricLine)[] =
+ [];
const lines = lrc.split('\n');
for (let i = 0, { length } = lines; i < length; i += 1) {
@@ -25,19 +22,12 @@ function parse(lrc: string) {
const timesPart = lyricMatch[1]; // [time][time]content --> [time][time]
const times = timesPart.split(']['); // [time1][time2] --> [time1 | time2]
for (const time of times) {
- const timeMatch = time.replace(/(\[|\])/g, '').match(LYRIC_TIME);
- const minute = timeMatch[1];
- const second = timeMatch[2];
- const centisecond = timeMatch[3] || '00'; // compatible with [00:00]
- const centisecondNumber =
- centisecond.length === 3 ? +centisecond : +centisecond * 10; // // compatible with [00:00.000]
const lyricLine: LyricLine = {
lineNumber: i,
raw,
type: LineType.LYRIC,
- startMillisecond:
- +minute * 60 * 1000 + +second * 1000 + centisecondNumber,
+ startMillisecond: timestampToMillsecond(time.replace(/[[\]]/g, '')),
content: lyricMatch[2],
};
parsedLines.push(lyricLine);
diff --git a/src/parse_enhanced.ts b/src/parse_enhanced.ts
new file mode 100644
index 0000000..8fb214d
--- /dev/null
+++ b/src/parse_enhanced.ts
@@ -0,0 +1,89 @@
+import {
+ EnhancedLyricLine,
+ InvalidLine,
+ LineType,
+ MetadataLine,
+ EnhancedWord,
+} from './constants';
+import parse from './parse';
+import { timestampToMillsecond } from './utils';
+
+/**
+ * <00:00.000>
+ * <00:00.00>
+ * <00:00>
+ */
+const ENHANCED_TIME = /<\d{2,}:\d{2}(?:\.(?:\d{2,3}))?>/g;
+
+function parseEnhanced(
+ lrc: string
+): (InvalidLine | MetadataLine | EnhancedLyricLine)[] {
+ const lines = parse(lrc);
+ return lines.map((line) => {
+ if (line.type === LineType.LYRIC) {
+ const words: EnhancedWord[] = [];
+
+ const wordTimeTagMatch = line.content.match(ENHANCED_TIME);
+
+ if (!wordTimeTagMatch) {
+ /**
+ * no time tag but having content is invalid
+ * like [time] xxxxx
+ * @author mebtte
+ */
+ if (line.content.trim().length) {
+ return {
+ type: LineType.INVALID,
+ lineNumber: line.lineNumber,
+ raw: line.raw,
+ } as InvalidLine;
+ }
+ } else {
+ const wordContents = line.content.split(ENHANCED_TIME);
+
+ /**
+ * ignore first emptiness or only-space
+ * @author mebtte
+ */
+ if (wordContents[0].trim().length === 0) {
+ wordContents.shift();
+ }
+
+ /**
+ * time tag' length should equal to content's length
+ * @author mebtte
+ */
+ if (wordTimeTagMatch.length !== wordContents.length) {
+ return {
+ type: LineType.INVALID,
+ lineNumber: line.lineNumber,
+ raw: line.raw,
+ } as InvalidLine;
+ }
+
+ for (let i = 0; i < wordTimeTagMatch.length; i += 1) {
+ const timestamp = wordTimeTagMatch[i];
+ const content = wordContents[i];
+ words.push({
+ index: i,
+ raw: `${timestamp}${content}`,
+ startMillisecond: timestampToMillsecond(
+ timestamp.replace(/[<>]/g, '')
+ ),
+ content,
+ });
+ }
+ }
+
+ return {
+ ...line,
+ type: LineType.ENHANCED_LYRIC,
+ words,
+ };
+ }
+
+ return line;
+ });
+}
+
+export default parseEnhanced;
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..bbfc6bf
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,11 @@
+const TIMESTAMP = /^(\d{2,}):(\d{2})(?:\.(\d{2,3}))?$/; // 00:00.000 | 00:00.00 | 00:00
+
+export function timestampToMillsecond(timestamp: string) {
+ const timeMatch = timestamp.match(TIMESTAMP);
+ const minute = timeMatch[1];
+ const second = timeMatch[2];
+ const centisecond = timeMatch[3] || '00'; // compatible with [00:00]
+ const centisecondNumber =
+ centisecond.length === 3 ? +centisecond : +centisecond * 10; // // compatible with [00:00.000]
+ return +minute * 60 * 1000 + +second * 1000 + centisecondNumber;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 10c508d..e11d32e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,8 +2,6 @@
"compilerOptions": {
"lib": ["ES2015"],
"outDir": "dist",
- "jsx": "react",
- "esModuleInterop": true,
"skipLibCheck": true
},
"exclude": ["./playground", "./dist"]