{"version":3,"sources":["../src/index.ts","../src/news.ts","../src/config.ts","../src/utils.ts"],"sourcesContent":["import { LodestoneNews } from './news'\n\nexport * from './config'\nexport * from './news'\n\n/**\n * Fetch the latest news from Lodestone.\n * @param {Object} option - The options to use when creating the LodestoneNews instance\n * @param {string} option.region - The region to fetch news from\n * @param {string} option.category - The category to fetch news from\n * @param {number} option.count - The number of news to fetch\n * @param {Object} option.request - The request options to use when fetching the news\n * @returns {Promise<News>} - News instance\n */\nexport const getNews = (option: Parameters<LodestoneNews['getNews']>[0]) =>\n  new LodestoneNews().getNews(option)\n\nexport default getNews\n","import { RequestOptions } from 'https'\n\nimport { load } from 'cheerio'\n\nimport defaultConfig, {\n  Config,\n  defaultRules,\n  News,\n  NewsList,\n  Rule,\n} from './config'\nimport { filter, getUrl } from './utils'\n\nexport const regions = ['na', 'eu', 'fr', 'de', 'jp'] as const\nexport type Region = typeof regions[number]\n\nexport interface Context {\n  region: Region\n  category: string\n  page: number\n  referer: string\n}\n\nexport class LodestoneNews {\n  protected rules: Record<string, Rule>\n  protected region: Region\n  protected count: number\n  protected request?: RequestOptions\n  constructor(config: Partial<Config> = {}) {\n    this.rules = { ...defaultRules, ...config.rules }\n    this.region = config.region ?? defaultConfig.region\n    this.count = config.count ?? defaultConfig.count\n  }\n\n  async getNews(option: {\n    region?: Region\n    category?: string\n    count?: number\n    request?: RequestOptions\n    before?: Date\n    after?: Date\n  }): Promise<News[]> {\n    const {\n      region = this.region,\n      category = 'topics',\n      count = this.count,\n      request = this.request ?? undefined,\n      before,\n      after,\n    } = option\n    const ret = []\n    let curPage = 1\n    while (ret.length < count) {\n      const news = await this.getPage({\n        region,\n        category,\n        page: curPage,\n        request,\n      })\n      const arr = before || after ? filter(news, before, after) : news\n      ret.push(...arr)\n      curPage = news.current + 1\n      if (curPage > news.total || (arr.length === 0 && ret.length !== 0)) {\n        break\n      }\n    }\n    return ret.slice(0, count)\n  }\n\n  async getPage(option: {\n    region: Region\n    category: string\n    page: number\n    request?: RequestOptions\n  }): Promise<NewsList> {\n    const { region, category, page, request } = option\n    if (regions.includes(region) === false) {\n      throw new Error(`Invalid locale: ${region}`)\n    }\n    if (!Object.prototype.hasOwnProperty.call(this.rules, category)) {\n      throw new Error(`Invalid category: ${category}`)\n    }\n    if (page <= 0) {\n      throw new Error(`Invalid page: ${page}`)\n    }\n\n    const ctx: Context = { region, category, page, referer: '' }\n    const rule = this.rules[category]\n    const url = typeof rule.url === 'function' ? rule.url(ctx) : rule.url\n    ctx.referer = url\n\n    const resp = await getUrl(url, request)\n    const $ = load(resp, { baseURI: url })\n    const rootNode = rule.rootNode($, ctx)\n    const pager = rule.page(rootNode, $, ctx)\n    const items = rule.items(rootNode, $, ctx)\n\n    const news = items as NewsList\n    news.current = pager.current\n    news.total = pager.total\n    return news\n  }\n}\n","import { Cheerio, CheerioAPI, Element } from 'cheerio'\n\nimport { Context, Region } from './news'\n\nconst i18n = {\n  pager: {\n    na: /Page (?<current>\\d+) of (?<total>\\d+)/,\n    eu: /Page (?<current>\\d+) of (?<total>\\d+)/,\n    fr: /Page (?<current>\\d+) \\/ (?<total>\\d+)/,\n    de: /Seite (?<current>\\d+) \\(von (?<total>\\d+)\\)/,\n    jp: /(?<current>\\d+)ページ \\/ (?<total>\\d+)ページ/,\n  },\n}\n\nconst baseRule: Pick<Rule, 'rootNode' | 'page' | 'items'> = {\n  rootNode: ($) =>\n    $('#news')\n      .children('div.ldst__bg')\n      .children('div.ldst__contents')\n      .children('div.ldst__main')\n      .children('div.news__content'),\n  page(el, $, context) {\n    const pager = el\n      .find('ul.btn__pager > li.btn__pager__current')\n      .first()\n      .text()\n    const m = i18n.pager[context.region].exec(pager)\n    return {\n      current: parseInt(m?.[1] ?? '0', 10),\n      total: parseInt(m?.[2] ?? '0', 10),\n    }\n  },\n  items(el, $, context) {\n    // There might be a \"Special Notice\" section, but we don't need it.\n    // So we have to find the news item without the \"parts__space--add\" class.\n    const items = el.find('ul:not(.parts__space--add) > li.news__list')\n    return items\n      .map((i, el) => {\n        const $a = $(el).find('a')\n\n        const title = $a.find('p.news__list--title').text()\n        const url = $a.prop('href')\n        if (!url) {\n          throw new Error(`No URL found for \"${title}\" in ${context.referer}`)\n        }\n\n        const $timeScript = $a.find('time.news__list--time > script').text()\n        const time = $timeScript.match(/ldst_strftime\\((\\d+), 'YMD'\\);/)?.[1]\n        if (!time) {\n          throw new Error(`No create time found for \"${title}\" in ${context.referer}`)\n        }\n        const epoch = parseInt(time, 10)\n        return {\n          title,\n          epoch,\n          date: new Date(epoch * 1000),\n          url,\n        }\n      })\n      .toArray()\n  },\n}\n\nexport const defaultRules: Record<string, Rule> = {\n  topics: {\n    ...baseRule,\n    url: ({ region, page }) =>\n      `https://${region}.finalfantasyxiv.com/lodestone/topics/?page=${page}`,\n    items(el, $, context) {\n      const items = el.find('li.news__list--topics')\n      return items\n        .map((i, el) => {\n          const $header = $(el).find('header.news__list--header')\n          const $title = $($header).find('p.news__list--title > a')\n\n          const title = $title.text()\n          const url = $title.prop('href')\n          if (!url) {\n            throw new Error(`No URL found for \"${title}\" in ${context.referer}`)\n          }\n\n          const $timeScript = $($header)\n            .find('time.news__list--time > script')\n            .text()\n          const time = $timeScript.match(/ldst_strftime\\((\\d+), 'YMD'\\);/)?.[1]\n          if (!time) {\n            throw new Error(`No create time found for \"${title}\" in ${context.referer}`)\n          }\n          const epoch = parseInt(time, 10)\n\n          return {\n            title,\n            epoch,\n            date: new Date(epoch * 1000),\n            url,\n          }\n        })\n        .toArray()\n    },\n  },\n  notices: {\n    ...baseRule,\n    url: ({ region, page }) =>\n      `https://${region}.finalfantasyxiv.com/lodestone/news/category/1?page=${page}`,\n  },\n  maintenance: {\n    ...baseRule,\n    url: ({ region, page }) =>\n      `https://${region}.finalfantasyxiv.com/lodestone/news/category/2?page=${page}`,\n  },\n  updates: {\n    ...baseRule,\n    url: ({ region, page }) =>\n      `https://${region}.finalfantasyxiv.com/lodestone/news/category/3?page=${page}`,\n  },\n  status: {\n    ...baseRule,\n    url: ({ region, page }) =>\n      `https://${region}.finalfantasyxiv.com/lodestone/news/category/4?page=${page}`,\n  },\n}\n\nconst config: Config = {\n  region: 'jp',\n  count: 5,\n  rules: defaultRules,\n}\n\nexport default config\n\nexport interface Config {\n  region: Region\n  count: number\n  rules: Record<string, Rule>\n}\n\nexport interface Rule {\n  url: string | ((context: Context) => string)\n  page: (el: Cheerio<Element>, $: CheerioAPI, ctx: Context) => Page\n  rootNode: ($: CheerioAPI, ctx: Context) => Cheerio<Element>\n  items: (l: Cheerio<Element>, $: CheerioAPI, ctx: Context) => News[]\n}\n\nexport interface News {\n  title: string\n  epoch: number\n  date: Date\n  url: string\n}\n\nexport interface Page {\n  current: number\n  total: number\n}\n\nexport interface NewsList extends Array<News>, Page {}\n","import http from 'http'\nimport https from 'https'\n\nimport { News } from './config'\n\nexport function filter(arr: News[], before?: Date, after?: Date): News[] {\n  return arr.filter(({ date }) => {\n    if (before && after) {\n      if (before < after) {\n        throw new Error(`\\`before\\`(${before}) should not be less than \\`after\\`(${after}).`)\n      }\n      if (date < before && date > after) return true\n    } else if ((before && date < before) || (after && date > after)) return true\n    return false\n  })\n}\n\nexport async function getUrl(url: string, options?: https.RequestOptions) {\n  return new Promise<string>((resolve, reject) => {\n    ;(url.startsWith('https://') ? https : http).get(\n      url,\n      { ...options },\n      (res) => {\n        let data = ''\n        res.setEncoding('utf8')\n        res.on('error', reject)\n        res.on('data', (chunk) => {\n          data += chunk as string\n        })\n        res.on('end', () => {\n          resolve(data)\n        })\n      },\n    )\n  })\n}\n"],"mappings":"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","names":["src_exports","__export","LodestoneNews","src_default","defaultRules","getNews","regions","__toCommonJS","import_cheerio","i18n","baseRule","$","el","context","pager","m","i","$a","title","url","time","epoch","defaultRules","region","page","$header","$title","config","config_default","import_http","import_https","filter","arr","before","after","date","getUrl","url","options","resolve","reject","https","http","res","data","chunk","regions","LodestoneNews","config","defaultRules","config_default","option","region","category","count","request","before","after","ret","curPage","news","arr","filter","page","ctx","rule","url","resp","getUrl","$","rootNode","pager","getNews","option","LodestoneNews","src_default"]}