diff --git a/src/@types/index.ts b/src/@types/index.ts index d563c923..67dd24ae 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -10,6 +10,7 @@ export * from "./format.legacy"; export * from "./format.owner"; export * from "./format.v1"; export * from "./IComment"; +export * from "./input-parser"; export * from "./IPlugins"; export * from "./options"; export * from "./renderer"; diff --git a/src/@types/input-parser.ts b/src/@types/input-parser.ts new file mode 100644 index 00000000..57cf8473 --- /dev/null +++ b/src/@types/input-parser.ts @@ -0,0 +1,6 @@ +import type { FormattedComment } from "@/@types/format.formatted"; + +export interface InputParser { + key: string[]; + parse: (input: unknown) => FormattedComment[]; +} diff --git a/src/input/empty.ts b/src/input/empty.ts new file mode 100644 index 00000000..ed2d7cd3 --- /dev/null +++ b/src/input/empty.ts @@ -0,0 +1,8 @@ +import type { FormattedComment, InputParser } from "@/@types"; + +export const EmptyParser: InputParser = { + key: ["empty"], + parse: (): FormattedComment[] => { + return []; + }, +}; diff --git a/src/input/formatted.ts b/src/input/formatted.ts new file mode 100644 index 00000000..8cb1213c --- /dev/null +++ b/src/input/formatted.ts @@ -0,0 +1,11 @@ +import { array, parse } from "valibot"; + +import type { FormattedComment, InputParser } from "@/@types"; +import { ZFormattedComment } from "@/@types"; + +export const FormattedParser: InputParser = { + key: ["formatted", "niconicome"], + parse: (input: unknown): FormattedComment[] => { + return parse(array(ZFormattedComment), input); + }, +}; diff --git a/src/input/index.ts b/src/input/index.ts new file mode 100644 index 00000000..9c0373a7 --- /dev/null +++ b/src/input/index.ts @@ -0,0 +1,19 @@ +import type { InputParser } from "@/@types"; + +import { EmptyParser } from "./empty"; +import { FormattedParser } from "./formatted"; +import { LegacyParser } from "./legacy"; +import { LegacyOwnerParser } from "./legacyOwner"; +import { OwnerParser } from "./owner"; +import { V1Parser } from "./v1"; +import { XmlDocumentParser } from "./xmlDocument"; + +export const parsers: InputParser[] = [ + EmptyParser, + FormattedParser, + LegacyParser, + LegacyOwnerParser, + OwnerParser, + V1Parser, + XmlDocumentParser, +]; diff --git a/src/input/legacy.ts b/src/input/legacy.ts new file mode 100644 index 00000000..2af662d2 --- /dev/null +++ b/src/input/legacy.ts @@ -0,0 +1,61 @@ +import { array, parse, safeParse } from "valibot"; + +import type { InputParser } from "@/@types"; +import { + type FormattedComment, + type RawApiResponse, + ZApiChat, + ZRawApiResponse, +} from "@/@types"; + +export const LegacyParser: InputParser = { + key: ["legacy"], + parse: (input) => { + return fromLegacy(parse(array(ZRawApiResponse), input)); + }, +}; + +/** + * ニコニコ公式のlegacy apiから帰ってきたデータ処理する + * @param data legacy apiから帰ってきたデータ + * @returns 変換後のデータ + */ +const fromLegacy = (data: RawApiResponse[]): FormattedComment[] => { + const data_: FormattedComment[] = [], + userList: string[] = []; + for (const _val of data) { + const val = safeParse(ZApiChat, _val.chat); + if (!val.success) continue; + const value = val.output; + if (value.deleted !== 1) { + const tmpParam: FormattedComment = { + id: value.no, + vpos: value.vpos, + content: value.content || "", + date: value.date, + date_usec: value.date_usec || 0, + owner: !value.user_id, + premium: value.premium === 1, + mail: [], + user_id: -1, + layer: -1, + is_my_post: false, + }; + if (value.mail) { + tmpParam.mail = value.mail.split(/\s+/g); + } + if (value.content.startsWith("/") && !value.user_id) { + tmpParam.mail.push("invisible"); + } + const isUserExist = userList.indexOf(value.user_id); + if (isUserExist === -1) { + tmpParam.user_id = userList.length; + userList.push(value.user_id); + } else { + tmpParam.user_id = isUserExist; + } + data_.push(tmpParam); + } + } + return data_; +}; diff --git a/src/input/legacyOwner.ts b/src/input/legacyOwner.ts new file mode 100644 index 00000000..13e367a6 --- /dev/null +++ b/src/input/legacyOwner.ts @@ -0,0 +1,55 @@ +import type { InputParser } from "@/@types"; +import { type FormattedComment } from "@/@types"; +import { InvalidFormatError } from "@/errors"; +import typeGuard from "@/typeGuard"; + +export const LegacyOwnerParser: InputParser = { + key: ["legacyOwner"], + parse: (input) => { + if (!typeGuard.legacyOwner.comments(input)) throw new InvalidFormatError(); + return fromLegacyOwner(input); + }, +}; + +/** + * 旧プレイヤーの投稿者コメントのエディターのデータを処理する + * @param data 旧投米のテキストデータ + * @returns 変換後のデータ + */ +const fromLegacyOwner = (data: string): FormattedComment[] => { + const data_: FormattedComment[] = [], + comments = data.split("\n"); + for (let i = 0, n = comments.length; i < n; i++) { + const value = comments[i]; + if (!value) continue; + const commentData = value.split(":"); + if (commentData.length < 3) { + continue; + } else if (commentData.length > 3) { + for (let j = 3, n = commentData.length; j < n; j++) { + commentData[2] += `:${commentData[j]}`; + } + } + const tmpParam: FormattedComment = { + id: i, + vpos: Number(commentData[0]) * 100, + content: commentData[2] ?? "", + date: i, + date_usec: 0, + owner: true, + premium: true, + mail: [], + user_id: -1, + layer: -1, + is_my_post: false, + }; + if (commentData[1]) { + tmpParam.mail = commentData[1].split(/[\s+]/g); + } + if (tmpParam.content.startsWith("/")) { + tmpParam.mail.push("invisible"); + } + data_.push(tmpParam); + } + return data_; +}; diff --git a/src/input/owner.ts b/src/input/owner.ts new file mode 100644 index 00000000..f3ba34cd --- /dev/null +++ b/src/input/owner.ts @@ -0,0 +1,82 @@ +import { array, parse } from "valibot"; + +import type { InputParser } from "@/@types"; +import { + type FormattedComment, + type OwnerComment, + ZOwnerComment, +} from "@/@types"; + +export const OwnerParser: InputParser = { + key: ["owner"], + parse: (input) => { + return fromOwner(parse(array(ZOwnerComment), input)); + }, +}; + +/** + * 投稿者コメントのエディターのデータを処理する + * @param data 投米のデータ + * @returns 変換後のデータ + */ +const fromOwner = (data: OwnerComment[]): FormattedComment[] => { + const data_: FormattedComment[] = []; + for (let i = 0, n = data.length; i < n; i++) { + const value = data[i]; + if (!value) continue; + const tmpParam: FormattedComment = { + id: i, + vpos: time2vpos(value.time), + content: value.comment, + date: i, + date_usec: 0, + owner: true, + premium: true, + mail: [], + user_id: -1, + layer: -1, + is_my_post: false, + }; + if (value.command) { + tmpParam.mail = value.command.split(/\s+/g); + } + if (tmpParam.content.startsWith("/")) { + tmpParam.mail.push("invisible"); + } + data_.push(tmpParam); + } + return data_; +}; + +/** + * 投稿者コメントのエディターは秒数の入力フォーマットに割りと色々対応しているのでvposに変換 + * @param input 分:秒.秒・分:秒・秒.秒・秒 + * @returns vpos + */ +const time2vpos = (input: string): number => { + const time = RegExp( + /^(?:(\d+):(\d+)\.(\d+)|(\d+):(\d+)|(\d+)\.(\d+)|(\d+))$/, + ).exec(input); + if (time) { + if ( + time[1] !== undefined && + time[2] !== undefined && + time[3] !== undefined + ) { + return ( + (Number(time[1]) * 60 + Number(time[2])) * 100 + + Number(time[3]) / Math.pow(10, time[3].length - 2) + ); + } else if (time[4] !== undefined && time[5] !== undefined) { + return (Number(time[4]) * 60 + Number(time[5])) * 100; + } else if (time[6] !== undefined && time[7] !== undefined) { + return ( + Number(time[6]) * 100 + + Number(time[7]) / Math.pow(10, time[7].length - 2) + ); + } else if (time[8] !== undefined) { + return Number(time[8]) * 100; + } + } + return 0; +}; diff --git a/src/input/v1.ts b/src/input/v1.ts new file mode 100644 index 00000000..a636674a --- /dev/null +++ b/src/input/v1.ts @@ -0,0 +1,60 @@ +import { array, parse } from "valibot"; + +import type { InputParser } from "@/@types"; +import { type FormattedComment, type V1Thread, ZV1Thread } from "@/@types"; + +export const V1Parser: InputParser = { + key: ["v1"], + parse: (input: unknown) => { + return fromV1(parse(array(ZV1Thread), input)); + }, +}; + +/** + * ニコニコ公式のv1 apiから帰ってきたデータ処理する + * data内threadsのデータを渡されることを想定 + * @param data v1 apiから帰ってきたデータ + * @returns 変換後のデータ + */ +const fromV1 = (data: V1Thread[]): FormattedComment[] => { + const data_: FormattedComment[] = [], + userList: string[] = []; + for (const item of data) { + const val = item.comments, + forkName = item.fork; + for (const value of val) { + const tmpParam: FormattedComment = { + id: value.no, + vpos: Math.floor(value.vposMs / 10), + content: value.body, + date: date2time(value.postedAt), + date_usec: 0, + owner: forkName === "owner", + premium: value.isPremium, + mail: value.commands, + user_id: -1, + layer: -1, + is_my_post: value.isMyPost, + }; + if (tmpParam.content.startsWith("/") && tmpParam.owner) { + tmpParam.mail.push("invisible"); + } + const isUserExist = userList.indexOf(value.userId); + if (isUserExist === -1) { + tmpParam.user_id = userList.length; + userList.push(value.userId); + } else { + tmpParam.user_id = isUserExist; + } + data_.push(tmpParam); + } + } + return data_; +}; + +/** + * v1 apiのpostedAtはISO 8601のtimestampなのでDate関数を使ってunix timestampに変換 + * @param date ISO 8601 timestamp + * @returns unix timestamp + */ +const date2time = (date: string): number => Math.floor(Date.parse(date) / 1000); diff --git a/src/input/xmlDocument.ts b/src/input/xmlDocument.ts new file mode 100644 index 00000000..04eca072 --- /dev/null +++ b/src/input/xmlDocument.ts @@ -0,0 +1,54 @@ +import type { FormattedComment, InputParser } from "@/@types"; +import { InvalidFormatError } from "@/errors"; +import typeGuard from "@/typeGuard"; + +export const XmlDocumentParser: InputParser = { + key: ["formatted", "niconicome"], + parse: (input: unknown): FormattedComment[] => { + if (!typeGuard.xmlDocument(input)) throw new InvalidFormatError(); + return parseXMLDocument(input); + }, +}; + +/** + * niconicome等が吐き出すxml形式のコメントデータを処理する + * @param data 吐き出されたxmlをDOMParserでparseFromStringしたもの + * @returns 変換後のデータ + */ +const parseXMLDocument = (data: XMLDocument): FormattedComment[] => { + const data_: FormattedComment[] = [], + userList: string[] = []; + let index = Array.from(data.documentElement.children).length; + for (const item of Array.from(data.documentElement.children)) { + if (item.nodeName !== "chat") continue; + const tmpParam: FormattedComment = { + id: Number(item.getAttribute("no")) || index++, + vpos: Number(item.getAttribute("vpos")), + content: item.textContent ?? "", + date: Number(item.getAttribute("date")) || 0, + date_usec: Number(item.getAttribute("date_usec")) || 0, + owner: !item.getAttribute("user_id"), + premium: item.getAttribute("premium") === "1", + mail: [], + user_id: -1, + layer: -1, + is_my_post: false, + }; + if (item.getAttribute("mail")) { + tmpParam.mail = item.getAttribute("mail")?.split(/\s+/g) ?? []; + } + if (tmpParam.content.startsWith("/") && tmpParam.owner) { + tmpParam.mail.push("invisible"); + } + const userId = item.getAttribute("user_id") ?? ""; + const isUserExist = userList.indexOf(userId); + if (isUserExist === -1) { + tmpParam.user_id = userList.length; + userList.push(userId); + } else { + tmpParam.user_id = isUserExist; + } + data_.push(tmpParam); + } + return data_; +}; diff --git a/src/inputParser.ts b/src/inputParser.ts index 773798df..57e7c14d 100755 --- a/src/inputParser.ts +++ b/src/inputParser.ts @@ -1,21 +1,6 @@ -import { array, parse, safeParse, ValiError } from "valibot"; - -import type { - FormattedComment, - InputFormatType, - OwnerComment, - RawApiResponse, - V1Thread, -} from "@/@types/"; -import { - ZApiChat, - ZFormattedComment, - ZOwnerComment, - ZRawApiResponse, - ZV1Thread, -} from "@/@types/"; -import { InvalidFormatError } from "@/errors/"; -import typeGuard from "@/typeGuard"; +import type { FormattedComment, InputFormatType } from "@/@types/"; +import { InvalidFormatError } from "@/errors"; +import { parsers } from "@/input"; /** * 入力されたデータを内部用のデータに変換 @@ -27,259 +12,12 @@ const convert2formattedComment = ( data: unknown, type: InputFormatType, ): FormattedComment[] => { - let result: FormattedComment[] = []; - try { - result = parseComments(data, type); - } catch (e) { - if (e instanceof ValiError) { - console.error("", e.issues); - } - } - return sort(result); -}; - -const parseComments = ( - data: unknown, - type: InputFormatType, -): FormattedComment[] => { - if (type === "empty" && data === undefined) { - return []; - } else if ( - (type === "XMLDocument" || type === "niconicome") && - typeGuard.xmlDocument(data) - ) { - return fromXMLDocument(data); - } else if (type === "formatted") { - return fromFormatted(parse(array(ZFormattedComment), data)); - } else if (type === "legacy") { - return fromLegacy(parse(array(ZRawApiResponse), data)); - } else if (type === "legacyOwner") { - if (!typeGuard.legacyOwner.comments(data)) throw new InvalidFormatError(); - return fromLegacyOwner(data); - } else if (type === "owner") { - return fromOwner(parse(array(ZOwnerComment), data)); - } else if (type === "v1") { - return fromV1(parse(array(ZV1Thread), data)); - } else { - throw new InvalidFormatError(); - } -}; - -/** - * niconicome等が吐き出すxml形式のコメントデータを処理する - * @param data 吐き出されたxmlをDOMParserでparseFromStringしたもの - * @returns 変換後のデータ - */ -const fromXMLDocument = (data: XMLDocument): FormattedComment[] => { - const data_: FormattedComment[] = [], - userList: string[] = []; - let index = Array.from(data.documentElement.children).length; - for (const item of Array.from(data.documentElement.children)) { - if (item.nodeName !== "chat") continue; - const tmpParam: FormattedComment = { - id: Number(item.getAttribute("no")) || index++, - vpos: Number(item.getAttribute("vpos")), - content: item.textContent ?? "", - date: Number(item.getAttribute("date")) || 0, - date_usec: Number(item.getAttribute("date_usec")) || 0, - owner: !item.getAttribute("user_id"), - premium: item.getAttribute("premium") === "1", - mail: [], - user_id: -1, - layer: -1, - is_my_post: false, - }; - if (item.getAttribute("mail")) { - tmpParam.mail = item.getAttribute("mail")?.split(/\s+/g) ?? []; - } - if (tmpParam.content.startsWith("/") && tmpParam.owner) { - tmpParam.mail.push("invisible"); - } - const userId = item.getAttribute("user_id") ?? ""; - const isUserExist = userList.indexOf(userId); - if (isUserExist === -1) { - tmpParam.user_id = userList.length; - userList.push(userId); - } else { - tmpParam.user_id = isUserExist; - } - data_.push(tmpParam); - } - return data_; -}; - -/** - * 内部処理用フォーマットを処理する - * 旧版だとデータにlayerとuser_idが含まれないので追加する - * @param data formattedからformattedに変換(不足データを追加) - * @returns 変換後のデータ - */ -const fromFormatted = (data: FormattedComment[]): FormattedComment[] => { - return data; -}; - -/** - * ニコニコ公式のlegacy apiから帰ってきたデータ処理する - * @param data legacy apiから帰ってきたデータ - * @returns 変換後のデータ - */ -const fromLegacy = (data: RawApiResponse[]): FormattedComment[] => { - const data_: FormattedComment[] = [], - userList: string[] = []; - for (const _val of data) { - const val = safeParse(ZApiChat, _val.chat); - if (!val.success) continue; - const value = val.output; - if (value.deleted !== 1) { - const tmpParam: FormattedComment = { - id: value.no, - vpos: value.vpos, - content: value.content || "", - date: value.date, - date_usec: value.date_usec || 0, - owner: !value.user_id, - premium: value.premium === 1, - mail: [], - user_id: -1, - layer: -1, - is_my_post: false, - }; - if (value.mail) { - tmpParam.mail = value.mail.split(/\s+/g); - } - if (value.content.startsWith("/") && !value.user_id) { - tmpParam.mail.push("invisible"); - } - const isUserExist = userList.indexOf(value.user_id); - if (isUserExist === -1) { - tmpParam.user_id = userList.length; - userList.push(value.user_id); - } else { - tmpParam.user_id = isUserExist; - } - data_.push(tmpParam); - } - } - return data_; -}; - -/** - * 旧プレイヤーの投稿者コメントのエディターのデータを処理する - * @param data 旧投米のテキストデータ - * @returns 変換後のデータ - */ -const fromLegacyOwner = (data: string): FormattedComment[] => { - const data_: FormattedComment[] = [], - comments = data.split("\n"); - for (let i = 0, n = comments.length; i < n; i++) { - const value = comments[i]; - if (!value) continue; - const commentData = value.split(":"); - if (commentData.length < 3) { - continue; - } else if (commentData.length > 3) { - for (let j = 3, n = commentData.length; j < n; j++) { - commentData[2] += `:${commentData[j]}`; - } - } - const tmpParam: FormattedComment = { - id: i, - vpos: Number(commentData[0]) * 100, - content: commentData[2] ?? "", - date: i, - date_usec: 0, - owner: true, - premium: true, - mail: [], - user_id: -1, - layer: -1, - is_my_post: false, - }; - if (commentData[1]) { - tmpParam.mail = commentData[1].split(/[\s+]/g); - } - if (tmpParam.content.startsWith("/")) { - tmpParam.mail.push("invisible"); - } - data_.push(tmpParam); - } - return data_; -}; - -/** - * 投稿者コメントのエディターのデータを処理する - * @param data 投米のデータ - * @returns 変換後のデータ - */ -const fromOwner = (data: OwnerComment[]): FormattedComment[] => { - const data_: FormattedComment[] = []; - for (let i = 0, n = data.length; i < n; i++) { - const value = data[i]; - if (!value) continue; - const tmpParam: FormattedComment = { - id: i, - vpos: time2vpos(value.time), - content: value.comment, - date: i, - date_usec: 0, - owner: true, - premium: true, - mail: [], - user_id: -1, - layer: -1, - is_my_post: false, - }; - if (value.command) { - tmpParam.mail = value.command.split(/\s+/g); - } - if (tmpParam.content.startsWith("/")) { - tmpParam.mail.push("invisible"); - } - data_.push(tmpParam); - } - return data_; -}; - -/** - * ニコニコ公式のv1 apiから帰ってきたデータ処理する - * data内threadsのデータを渡されることを想定 - * @param data v1 apiから帰ってきたデータ - * @returns 変換後のデータ - */ -const fromV1 = (data: V1Thread[]): FormattedComment[] => { - const data_: FormattedComment[] = [], - userList: string[] = []; - for (const item of data) { - const val = item.comments, - forkName = item.fork; - for (const value of val) { - const tmpParam: FormattedComment = { - id: value.no, - vpos: Math.floor(value.vposMs / 10), - content: value.body, - date: date2time(value.postedAt), - date_usec: 0, - owner: forkName === "owner", - premium: value.isPremium, - mail: value.commands, - user_id: -1, - layer: -1, - is_my_post: value.isMyPost, - }; - if (tmpParam.content.startsWith("/") && tmpParam.owner) { - tmpParam.mail.push("invisible"); - } - const isUserExist = userList.indexOf(value.userId); - if (isUserExist === -1) { - tmpParam.user_id = userList.length; - userList.push(value.userId); - } else { - tmpParam.user_id = isUserExist; - } - data_.push(tmpParam); + for (const parser of parsers) { + if (parser.key.includes(type)) { + return sort(parser.parse(data)); } } - return data_; + throw new InvalidFormatError(); }; /** @@ -302,45 +40,4 @@ const sort = (data: FormattedComment[]): FormattedComment[] => { return data; }; -/** - * 投稿者コメントのエディターは秒数の入力フォーマットに割りと色々対応しているのでvposに変換 - * @param input 分:秒.秒・分:秒・秒.秒・秒 - * @returns vpos - */ -const time2vpos = (input: string): number => { - const time = RegExp( - /^(?:(\d+):(\d+)\.(\d+)|(\d+):(\d+)|(\d+)\.(\d+)|(\d+))$/, - ).exec(input); - if (time) { - if ( - time[1] !== undefined && - time[2] !== undefined && - time[3] !== undefined - ) { - return ( - (Number(time[1]) * 60 + Number(time[2])) * 100 + - Number(time[3]) / Math.pow(10, time[3].length - 2) - ); - } else if (time[4] !== undefined && time[5] !== undefined) { - return (Number(time[4]) * 60 + Number(time[5])) * 100; - } else if (time[6] !== undefined && time[7] !== undefined) { - return ( - Number(time[6]) * 100 + - Number(time[7]) / Math.pow(10, time[7].length - 2) - ); - } else if (time[8] !== undefined) { - return Number(time[8]) * 100; - } - } - return 0; -}; - -/** - * v1 apiのpostedAtはISO 8601のtimestampなのでDate関数を使ってunix timestampに変換 - * @param date ISO 8601 timestamp - * @returns unix timestamp - */ -const date2time = (date: string): number => - Math.floor(new Date(date).getTime() / 1000); - export default convert2formattedComment;