parent
eae2c7f459
commit
958dd60c75
37 changed files with 12360 additions and 153 deletions
|
@ -9,3 +9,7 @@ insert_final_newline = true
|
|||
max_line_length = 120
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -4,3 +4,8 @@ private/
|
|||
.nyc_output/
|
||||
|
||||
*.log
|
||||
|
||||
# Docs are generated and deployed in gh-pages branch.
|
||||
|
||||
docs/*
|
||||
!docs/.nojekyll
|
||||
|
|
0
docs/.nojekyll
Normal file
0
docs/.nojekyll
Normal file
3
packages/tl-reference/.gitignore
vendored
Normal file
3
packages/tl-reference/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
public
|
6
packages/tl-reference/README.md
Normal file
6
packages/tl-reference/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# @mtcute/tl-reference
|
||||
|
||||
[https://mt.tei.su/tl](https://mt.tei.su/tl)
|
||||
|
||||
Small Gatsby application that allows easy browsing
|
||||
through Telegram APIs.
|
6
packages/tl-reference/data/README.md
Normal file
6
packages/tl-reference/data/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
This directory contains historical data about the TL schema.
|
||||
|
||||
That is, it contains history of the schema over time and pre-calculated difference
|
||||
between consecutive versions.
|
||||
|
||||
The files are generated with `fetch-history.js` script and are not in the repository.
|
1
packages/tl-reference/data/diffs/.gitignore
vendored
Normal file
1
packages/tl-reference/data/diffs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.json
|
1
packages/tl-reference/data/history/.gitignore
vendored
Normal file
1
packages/tl-reference/data/history/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.json
|
34
packages/tl-reference/gatsby-config.js
Normal file
34
packages/tl-reference/gatsby-config.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: 'TL reference',
|
||||
},
|
||||
pathPrefix: '/tl',
|
||||
plugins: [
|
||||
'gatsby-theme-material-ui',
|
||||
'gatsby-transformer-json',
|
||||
'gatsby-plugin-sass',
|
||||
'gatsby-plugin-react-helmet',
|
||||
{
|
||||
resolve: 'gatsby-plugin-nprogress',
|
||||
options: {
|
||||
color: 'white'
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-source-filesystem',
|
||||
options: {
|
||||
path: './data',
|
||||
ignore: [
|
||||
'./data/history/last-fetched.json',
|
||||
'./data/README.md',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-layout',
|
||||
options: {
|
||||
component: require.resolve('./src/layout.tsx'),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
137
packages/tl-reference/gatsby-node.js
Normal file
137
packages/tl-reference/gatsby-node.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
const rawSchema = require('@mtcute/tl/raw-schema')
|
||||
const rawErrors = require('@mtcute/tl/raw-errors')
|
||||
const path = require('path')
|
||||
const { convertToArrays, prepareData } = require('./scripts/prepare-data')
|
||||
|
||||
const TL_NODE_TYPE = 'TlObject'
|
||||
|
||||
exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
|
||||
const { createNode } = actions
|
||||
|
||||
function createForNs(ns, prefix = '') {
|
||||
ns.classes.forEach((cls) => {
|
||||
createNode({
|
||||
...cls,
|
||||
id: createNodeId(`${TL_NODE_TYPE}-class-${prefix}${cls.name}`),
|
||||
parent: null,
|
||||
children: [],
|
||||
type: 'class',
|
||||
prefix,
|
||||
internal: {
|
||||
type: TL_NODE_TYPE,
|
||||
content: JSON.stringify(cls),
|
||||
contentDigest: createContentDigest(cls),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
ns.methods.forEach((cls) => {
|
||||
createNode({
|
||||
...cls,
|
||||
id: createNodeId(`${TL_NODE_TYPE}-method-${prefix}${cls.name}`),
|
||||
parent: null,
|
||||
children: [],
|
||||
type: 'method',
|
||||
prefix,
|
||||
internal: {
|
||||
type: TL_NODE_TYPE,
|
||||
content: JSON.stringify(cls),
|
||||
contentDigest: createContentDigest(cls),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
ns.unions.forEach((cls) => {
|
||||
createNode({
|
||||
...cls,
|
||||
name: cls.type,
|
||||
id: createNodeId(`${TL_NODE_TYPE}-union-${prefix}${cls.type}`),
|
||||
parent: null,
|
||||
children: [],
|
||||
type: 'union',
|
||||
prefix,
|
||||
internal: {
|
||||
type: TL_NODE_TYPE,
|
||||
content: JSON.stringify(cls),
|
||||
contentDigest: createContentDigest(cls),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const mtproto = convertToArrays(rawSchema.mtproto)
|
||||
const api = convertToArrays(rawSchema.api)
|
||||
|
||||
prepareData(mtproto)
|
||||
prepareData(api)
|
||||
|
||||
createForNs(mtproto, 'mtproto/')
|
||||
createForNs(api)
|
||||
}
|
||||
|
||||
const TLObject = path.resolve('./src/templates/tl-object.tsx')
|
||||
const TlTypesList = path.resolve('./src/templates/tl-types-list.tsx')
|
||||
|
||||
exports.createPages = async ({ graphql, actions }) => {
|
||||
const result = await graphql(`
|
||||
query {
|
||||
allTlObject {
|
||||
nodes {
|
||||
prefix
|
||||
name
|
||||
type
|
||||
namespace
|
||||
subtypes
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
result.data.allTlObject.nodes.forEach((node) => {
|
||||
actions.createPage({
|
||||
path: `${node.prefix}${node.type}/${node.name}`,
|
||||
component: TLObject,
|
||||
context: {
|
||||
...node,
|
||||
hasSubtypes: !!node.subtypes,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const result2 = await graphql(`
|
||||
query {
|
||||
allTlObject {
|
||||
group(field: prefix) {
|
||||
fieldValue
|
||||
nodes {
|
||||
namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
result2.data.allTlObject.group.forEach(({ fieldValue: prefix, nodes }) => {
|
||||
const namespaces = [...new Set(nodes.map(i => i.namespace))]
|
||||
|
||||
namespaces.forEach((ns) => {
|
||||
let namespace
|
||||
if (ns === '$root') namespace = ''
|
||||
else namespace = '/' + ns
|
||||
|
||||
;(['types', 'methods']).forEach((type) => {
|
||||
actions.createPage({
|
||||
path: `${prefix}${type}${namespace}`,
|
||||
component: TlTypesList,
|
||||
context: {
|
||||
prefix,
|
||||
ns,
|
||||
type,
|
||||
isTypes: type === 'types',
|
||||
isMethods: type === 'methods'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
43
packages/tl-reference/package.json
Normal file
43
packages/tl-reference/package.json
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "tl-reference",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "TL reference",
|
||||
"author": "Alisa Sireneva <me@tei.su>",
|
||||
"scripts": {
|
||||
"develop": "gatsby develop --port 4040",
|
||||
"start": "gatsby develop",
|
||||
"build": "gatsby build --prefix-paths",
|
||||
"postbuild": "(rm -rf ../../docs/tl || rd /s /q ..\\..\\docs\\tl) && (mv public ../../docs/tl || move public ../../docs/tl)",
|
||||
"serve": "gatsby serve --prefix-paths",
|
||||
"clean": "gatsby clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/react-helmet": "^6.1.1",
|
||||
"gatsby": "^3.2.1",
|
||||
"gatsby-cli": "^3.2.0",
|
||||
"gatsby-source-filesystem": "^3.2.0",
|
||||
"gatsby-transformer-json": "^3.2.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"@mtcute/tl": "^0.0.0",
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"gatsby-theme-material-ui": "^2.0.1",
|
||||
"gatsby-plugin-layout": "^2.2.0",
|
||||
"gatsby-plugin-nprogress": "^3.3.0",
|
||||
"sass": "^1.32.8",
|
||||
"gatsby-plugin-sass": "^4.2.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"gatsby-plugin-react-helmet": "^4.3.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"lodash.throttle": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": "^2.6.1",
|
||||
"marked": "^2.0.3"
|
||||
}
|
||||
}
|
388
packages/tl-reference/scripts/fetch-history.js
Normal file
388
packages/tl-reference/scripts/fetch-history.js
Normal file
|
@ -0,0 +1,388 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { convertTlToJson } = require('../../tl/scripts/generate-schema')
|
||||
const fetch = require('node-fetch')
|
||||
const qs = require('querystring')
|
||||
const { convertToArrays } = require('./prepare-data')
|
||||
|
||||
const UNIX_0 = '1970-01-01T00:00:00Z'
|
||||
const CURRENT_FILE = 'Telegram/Resources/tl/api.tl'
|
||||
const FILES = [
|
||||
'Telegram/SourceFiles/mtproto/scheme.tl',
|
||||
'Telegram/Resources/scheme.tl',
|
||||
CURRENT_FILE,
|
||||
]
|
||||
|
||||
async function getLastFetched() {
|
||||
return fs.promises
|
||||
.readFile(path.join(__dirname, '../data/history/last-fetched.json'), 'utf8')
|
||||
.then((res) => JSON.parse(res))
|
||||
.catch(() => ({
|
||||
...FILES.reduce((a, b) => {
|
||||
a[b] = UNIX_0
|
||||
return a
|
||||
}, {}),
|
||||
}))
|
||||
}
|
||||
|
||||
async function updateLastFetched(file, time) {
|
||||
return getLastFetched().then((state) =>
|
||||
fs.promises.writeFile(
|
||||
path.join(__dirname, '../data/history/last-fetched.json'),
|
||||
JSON.stringify({
|
||||
...state,
|
||||
[file]: time,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function getFileContent(file, commit) {
|
||||
return fetch(
|
||||
`https://raw.githubusercontent.com/telegramdesktop/tdesktop/${commit}/${file}`
|
||||
).then((r) => r.text())
|
||||
}
|
||||
|
||||
async function parseRemoteTl(file, commit) {
|
||||
let content = await getFileContent(file, commit)
|
||||
if (content === '404: Not Found') return null
|
||||
|
||||
let layer = (function () {
|
||||
const m = content.match(/^\/\/ LAYER (\d+)/m)
|
||||
if (m) return m[1]
|
||||
return null
|
||||
})()
|
||||
if (!layer) {
|
||||
// older files did not contain layer number in comment.
|
||||
if (content.match(/invokeWithLayer#da9b0d0d/)) {
|
||||
// if this is present, then the layer number is available in
|
||||
// Telegram/SourceFiles/mtproto/mtpCoreTypes.h
|
||||
let mtpCoreTypes = await getFileContent(
|
||||
'Telegram/SourceFiles/mtproto/mtpCoreTypes.h',
|
||||
commit
|
||||
)
|
||||
if (mtpCoreTypes === '404: Not Found') {
|
||||
mtpCoreTypes = await getFileContent(
|
||||
'Telegram/SourceFiles/mtproto/core_types.h',
|
||||
commit
|
||||
)
|
||||
}
|
||||
const m = mtpCoreTypes.match(
|
||||
/^static const mtpPrime mtpCurrentLayer = (\d+);$/m
|
||||
)
|
||||
if (!m)
|
||||
throw new Error(
|
||||
`Could not determine layer number for file ${file} at commit ${commit}`
|
||||
)
|
||||
layer = m[1]
|
||||
} else {
|
||||
// even older files on ancient layers
|
||||
// layer number is the largest available invokeWithLayerN constructor
|
||||
let max = 0
|
||||
content.replace(/invokeWithLayer(\d+)#[0-f]+/g, (_, $1) => {
|
||||
$1 = parseInt($1)
|
||||
if ($1 > max) max = $1
|
||||
})
|
||||
if (max === 0)
|
||||
throw new Error(
|
||||
`Could not determine layer number for file ${file} at commit ${commit}`
|
||||
)
|
||||
layer = max + ''
|
||||
}
|
||||
}
|
||||
|
||||
if (content.match(/bad_server_salt#/)) {
|
||||
// this is an older file that contained both mtproto and api
|
||||
// since we are only interested in api, remove the mtproto part
|
||||
const lines = content.split('\n')
|
||||
const apiIdx = lines.indexOf('///////// Main application API')
|
||||
if (apiIdx === -1)
|
||||
throw new Error('Could not find split point for combined file')
|
||||
content = lines.slice(apiIdx).join('\n')
|
||||
}
|
||||
|
||||
return {
|
||||
layer,
|
||||
content,
|
||||
tl: await convertTlToJson(content, 'api', true),
|
||||
}
|
||||
}
|
||||
|
||||
function createTlDifference(old, mod) {
|
||||
const diff = {
|
||||
added: { classes: [], methods: [], unions: [] },
|
||||
removed: { classes: [], methods: [], unions: [] },
|
||||
modified: { classes: [], methods: [], unions: [] },
|
||||
}
|
||||
|
||||
old = convertToArrays(old.tl)
|
||||
mod = convertToArrays(mod.tl)
|
||||
|
||||
// create index for both old and mod
|
||||
const { oldIndex, modIndex } = (function () {
|
||||
function createIndex(it) {
|
||||
let ret = {}
|
||||
it.classes.forEach((obj) => {
|
||||
obj.uid = 'c_' + obj.name
|
||||
obj._type = 'classes'
|
||||
ret[obj.uid] = obj
|
||||
})
|
||||
it.methods.forEach((obj) => {
|
||||
obj.uid = 'm_' + obj.name
|
||||
obj._type = 'methods'
|
||||
ret[obj.uid] = obj
|
||||
})
|
||||
it.unions.forEach((obj) => {
|
||||
obj.uid = 'u_' + obj.type
|
||||
obj._type = 'unions'
|
||||
ret[obj.uid] = obj
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
return {
|
||||
oldIndex: createIndex(old),
|
||||
modIndex: createIndex(mod),
|
||||
}
|
||||
})()
|
||||
|
||||
// find difference between constructor arguments
|
||||
function createArgsDifference(old, mod) {
|
||||
const diff = {
|
||||
added: [],
|
||||
removed: [],
|
||||
modified: [],
|
||||
}
|
||||
|
||||
const { oldIndex, modIndex } = (function () {
|
||||
function createIndex(obj) {
|
||||
const ret = {}
|
||||
if (obj.arguments)
|
||||
obj.arguments.forEach((arg) => (ret[arg.name] = arg))
|
||||
return ret
|
||||
}
|
||||
|
||||
return {
|
||||
oldIndex: createIndex(old),
|
||||
modIndex: createIndex(mod),
|
||||
}
|
||||
})()
|
||||
|
||||
Object.keys(modIndex).forEach((argName) => {
|
||||
if (!(argName in oldIndex)) {
|
||||
diff.added.push(modIndex[argName])
|
||||
} else {
|
||||
const old = oldIndex[argName]
|
||||
const mod = modIndex[argName]
|
||||
if (
|
||||
old.type !== mod.type ||
|
||||
old.optional !== mod.optional ||
|
||||
mod.predicate !== mod.predicate
|
||||
) {
|
||||
diff.modified.push({
|
||||
name: argName,
|
||||
old: old,
|
||||
new: mod,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(oldIndex).forEach((argName) => {
|
||||
if (!(argName in modIndex)) {
|
||||
diff.removed.push(oldIndex[argName])
|
||||
}
|
||||
})
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
Object.keys(modIndex).forEach((uid) => {
|
||||
if (!(uid in oldIndex)) {
|
||||
diff.added[modIndex[uid]._type].push(modIndex[uid])
|
||||
} else {
|
||||
const old = oldIndex[uid]
|
||||
const mod = modIndex[uid]
|
||||
|
||||
const localDiff = {}
|
||||
|
||||
const argDiff = createArgsDifference(old, mod)
|
||||
if (
|
||||
argDiff.removed.length ||
|
||||
argDiff.added.length ||
|
||||
argDiff.modified.length
|
||||
) {
|
||||
localDiff.arguments = argDiff
|
||||
}
|
||||
|
||||
if (old.id !== mod.id) localDiff.id = { old: old.id, new: mod.id }
|
||||
if (old.type !== mod.type)
|
||||
localDiff.type = { old: old.type, new: mod.type }
|
||||
if (old.returns !== mod.returns)
|
||||
localDiff.returns = { old: old.returns, new: mod.returns }
|
||||
|
||||
if (Object.keys(localDiff).length) {
|
||||
localDiff.name = old.name
|
||||
diff.modified[oldIndex[uid]._type].push(localDiff)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(oldIndex).forEach((uid) => {
|
||||
if (!(uid in modIndex)) {
|
||||
diff.removed[oldIndex[uid]._type].push(oldIndex[uid])
|
||||
}
|
||||
})
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
function fileSafeDateFormat(date) {
|
||||
date = new Date(date)
|
||||
return date.toISOString().replace(/[\-:]|\.\d\d\d/g, '')
|
||||
}
|
||||
|
||||
function shortSha(sha) {
|
||||
return sha.substr(0, 7)
|
||||
}
|
||||
|
||||
async function fetchHistory(file, since, defaultParent = null) {
|
||||
const history = await (async function () {
|
||||
const ret = []
|
||||
let page = 1
|
||||
while (true) {
|
||||
const chunk = await fetch(
|
||||
`https://api.github.com/repos/telegramdesktop/tdesktop/commits?` +
|
||||
qs.stringify({
|
||||
since,
|
||||
path: file,
|
||||
per_page: 100,
|
||||
page,
|
||||
})
|
||||
).then((r) => r.json())
|
||||
|
||||
if (!chunk.length) break
|
||||
|
||||
ret.push(...chunk)
|
||||
page += 1
|
||||
}
|
||||
|
||||
return ret
|
||||
})()
|
||||
|
||||
// should not happen
|
||||
if (history.length === 0) throw new Error('history is empty')
|
||||
|
||||
const filename = (schema, commit) =>
|
||||
`layer${schema.layer}-${fileSafeDateFormat(
|
||||
commit.commit.committer.date
|
||||
)}-${shortSha(commit.sha)}.json`
|
||||
|
||||
function writeSchemaToFile(schema, commit) {
|
||||
return fs.promises.writeFile(
|
||||
path.join(__dirname, `../data/history/${filename(schema, commit)}`),
|
||||
JSON.stringify({
|
||||
tl: JSON.stringify(schema.tl),
|
||||
layer: parseInt(schema.layer),
|
||||
content: schema.content,
|
||||
// idk where parent: '00' comes from but whatever
|
||||
parent: schema.parent && schema.parent !== '00' ? schema.parent : defaultParent,
|
||||
source: {
|
||||
file,
|
||||
date: commit.commit.committer.date,
|
||||
commit: commit.sha,
|
||||
message: commit.message,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let base = history.pop()
|
||||
let baseSchema = await parseRemoteTl(file, base.sha)
|
||||
let baseFilename = () => filename(baseSchema, base)
|
||||
|
||||
try {
|
||||
await fs.promises.access(
|
||||
path.join(__dirname, `../data/history/${baseFilename()}`),
|
||||
fs.F_OK
|
||||
)
|
||||
} catch (e) {
|
||||
await writeSchemaToFile(baseSchema, base)
|
||||
}
|
||||
|
||||
while (history.length) {
|
||||
const next = history.pop()
|
||||
const nextSchema = await parseRemoteTl(file, next.sha)
|
||||
if (!nextSchema) break
|
||||
|
||||
const diff = createTlDifference(baseSchema, nextSchema)
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(
|
||||
__dirname,
|
||||
`../data/diffs/${shortSha(base.sha)}-${shortSha(next.sha)}.json`
|
||||
),
|
||||
JSON.stringify({
|
||||
...diff,
|
||||
// yeah they sometimes update schema w/out changing layer number
|
||||
layer:
|
||||
baseSchema.layer === nextSchema.layer
|
||||
? undefined
|
||||
: nextSchema.layer,
|
||||
})
|
||||
)
|
||||
|
||||
nextSchema.parent = baseFilename()
|
||||
base = next
|
||||
baseSchema = nextSchema
|
||||
await updateLastFetched(file, base.commit.committer.date)
|
||||
await writeSchemaToFile(baseSchema, base)
|
||||
console.log(
|
||||
'Fetched commit %s, file %s (%s)',
|
||||
shortSha(base.sha),
|
||||
file,
|
||||
base.commit.committer.date
|
||||
)
|
||||
}
|
||||
|
||||
if (file !== CURRENT_FILE) {
|
||||
await updateLastFetched(file, 'DONE:' + baseFilename())
|
||||
}
|
||||
|
||||
console.log('No more commits for %s', file)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const last = await getLastFetched()
|
||||
for (let i = 0; i < FILES.length; i++) {
|
||||
const file = FILES[i]
|
||||
const prev = FILES[i - 1]
|
||||
if (!last[file].startsWith('DONE')) {
|
||||
let parent = prev ? last[prev].split(':')[1] : null
|
||||
await fetchHistory(file, last[file], parent)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Creating reverse links ("next" field)')
|
||||
for (const file of await fs.promises.readdir(
|
||||
path.join(__dirname, '../data/history')
|
||||
)) {
|
||||
if (!file.startsWith('layer')) continue
|
||||
|
||||
const fullPath = path.join(__dirname, '../data/history', file)
|
||||
const json = JSON.parse(await fs.promises.readFile(fullPath, 'utf-8'))
|
||||
if (json.parent) {
|
||||
const parentPath = path.join(__dirname, '../data/history', json.parent)
|
||||
const parentJson = JSON.parse(
|
||||
await fs.promises.readFile(
|
||||
parentPath,
|
||||
'utf-8'
|
||||
)
|
||||
)
|
||||
parentJson.next = parentPath
|
||||
await fs.promises.writeFile(parentPath, JSON.stringify(parentJson))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
156
packages/tl-reference/scripts/prepare-data.js
Normal file
156
packages/tl-reference/scripts/prepare-data.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
// converts object-based schema to array-based
|
||||
function convertToArrays(ns) {
|
||||
const ret = {
|
||||
classes: [],
|
||||
methods: [],
|
||||
unions: [],
|
||||
}
|
||||
|
||||
Object.entries(ns).forEach(([ns, content]) => {
|
||||
const prefix = ns === '$root' ? '' : `${ns}.`
|
||||
|
||||
content.classes.forEach((cls) => {
|
||||
cls.rawName = cls.name
|
||||
cls.name = prefix + cls.name
|
||||
cls.namespace = ns
|
||||
ret.classes.push(cls)
|
||||
})
|
||||
|
||||
content.methods.forEach((cls) => {
|
||||
cls.rawName = cls.name
|
||||
cls.name = prefix + cls.name
|
||||
cls.namespace = ns
|
||||
ret.methods.push(cls)
|
||||
})
|
||||
|
||||
content.unions.forEach((cls) => {
|
||||
cls.rawName = cls.type
|
||||
cls.type = prefix + cls.type
|
||||
cls.namespace = ns
|
||||
ret.unions.push(cls)
|
||||
})
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
const marked = require('marked')
|
||||
|
||||
const pascalToCamel = (s) => s[0].toLowerCase() + s.substr(1)
|
||||
const camelToSnake = (str) =>
|
||||
str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
||||
const camelToPascal = (s) => s[0].toUpperCase() + s.substr(1)
|
||||
|
||||
function renderDescription(description) {
|
||||
return marked.parseInline(
|
||||
description.replace(/{@link (.+?)}/g, (_, name) => {
|
||||
if (name.startsWith('tl.')) {
|
||||
let [ns, type] = name.substr(3).split('.')
|
||||
if (!type) {
|
||||
type = ns
|
||||
ns = undefined
|
||||
}
|
||||
|
||||
let path, displayName, m
|
||||
if ((m = type.match(/^Raw([A-Za-z0-9_]+?)(Request)?$/))) {
|
||||
const [, name, isMethod] = m
|
||||
path = `${ns === 'mtproto' ? ns : ''}/${
|
||||
isMethod ? 'method' : 'class'
|
||||
}/${ns !== 'mtproto' ? ns + '.' : ''}${pascalToCamel(name)}`
|
||||
displayName =
|
||||
(ns ? ns + (ns === 'mtproto' ? '/' : '.') : '') +
|
||||
pascalToCamel(name)
|
||||
} else if ((m = type.match(/^Type([A-Za-z0-9_]+?)$/))) {
|
||||
path = `${ns === 'mtproto' ? ns : ''}/union/${
|
||||
ns !== 'mtproto' ? ns + '.' : ''
|
||||
}${m[1]}`
|
||||
displayName =
|
||||
(ns ? ns + (ns === 'mtproto' ? '/' : '.') : '') +
|
||||
pascalToCamel(name)
|
||||
}
|
||||
|
||||
if (path) {
|
||||
return `[${displayName}](/${path})`
|
||||
}
|
||||
}
|
||||
return `\`${name}\``
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function prepareData(data) {
|
||||
Object.values(data).forEach((arr) =>
|
||||
arr.forEach((item) => {
|
||||
// add hex constructor id
|
||||
if (item.id) item.tlId = item.id.toString(16).padStart(8, '0')
|
||||
|
||||
// add typescript types for the item and arguments
|
||||
// basically copy-pasted from generate-types.js
|
||||
const prefix_ = item.prefix === 'mtproto/' ? 'mt_' : ''
|
||||
let baseTypePrefix =
|
||||
item.prefix === 'mtproto/' ? 'tl.mtproto.' : 'tl.'
|
||||
|
||||
const makePascalCaseNotNamespace = (type) => {
|
||||
let split = type.split('.')
|
||||
let name = split.pop()
|
||||
let ns = split
|
||||
|
||||
if (!ns.length) {
|
||||
if (name[0].match(/[A-Z]/))
|
||||
// this is union/alias
|
||||
return 'Type' + name
|
||||
|
||||
return 'Raw' + camelToPascal(name)
|
||||
}
|
||||
if (name[0].match(/[A-Z]/)) return ns.join('.') + '.Type' + name
|
||||
return ns.join('.') + '.Raw' + camelToPascal(name)
|
||||
}
|
||||
const fullTypeName = (type) => {
|
||||
if (type === 'X') return 'any'
|
||||
if (type[0] === '%') type = type.substr(1)
|
||||
if (prefix_ === 'mt_' && type === 'Object') return 'tl.TlObject'
|
||||
if (
|
||||
type === 'number' ||
|
||||
type === 'any' ||
|
||||
type === 'Long' ||
|
||||
type === 'RawLong' ||
|
||||
type === 'Int128' ||
|
||||
type === 'Int256' ||
|
||||
type === 'Double' ||
|
||||
type === 'string' ||
|
||||
type === 'Buffer' ||
|
||||
type.match(/^(boolean|true|false)$/)
|
||||
)
|
||||
return type
|
||||
if (type.endsWith('[]')) {
|
||||
let wrap = type.substr(0, type.length - 2)
|
||||
return fullTypeName(wrap) + '[]'
|
||||
}
|
||||
|
||||
return baseTypePrefix + makePascalCaseNotNamespace(type)
|
||||
}
|
||||
|
||||
if (item.subtypes) {
|
||||
item.ts = 'Type' + item.rawName
|
||||
} else {
|
||||
item.ts =
|
||||
'Raw' +
|
||||
camelToPascal(item.rawName) +
|
||||
(item.returns ? 'Request' : '')
|
||||
item.underscore = prefix_ + item.name
|
||||
}
|
||||
|
||||
// render descriptions in markdown
|
||||
if (item.description)
|
||||
item.description = renderDescription(item.description)
|
||||
if (item.arguments)
|
||||
item.arguments.forEach((arg) => {
|
||||
if (arg.description)
|
||||
arg.description = renderDescription(arg.description)
|
||||
arg.ts = fullTypeName(arg.type)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = { convertToArrays, prepareData }
|
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
createStyles,
|
||||
fade,
|
||||
InputBase,
|
||||
InputBaseProps,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core'
|
||||
import React from 'react'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
search: {
|
||||
position: 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: fade(theme.palette.common.white, 0.25),
|
||||
},
|
||||
marginRight: theme.spacing(2),
|
||||
marginLeft: 0,
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
marginLeft: theme.spacing(3),
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
searchIcon: {
|
||||
padding: theme.spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputRoot: {
|
||||
color: 'inherit',
|
||||
},
|
||||
inputInput: {
|
||||
padding: theme.spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '40ch',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export function ActionBarSearchField(
|
||||
params: Partial<InputBaseProps> & { inputRef?: React.Ref<any> }
|
||||
): React.ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classes.search}>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<InputBase
|
||||
ref={params.inputRef}
|
||||
placeholder="Search…"
|
||||
classes={{
|
||||
root: classes.inputRoot,
|
||||
input: classes.inputInput,
|
||||
}}
|
||||
inputProps={{ 'aria-label': 'search' }}
|
||||
{...params}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
35
packages/tl-reference/src/components/fuse-highlight.tsx
Normal file
35
packages/tl-reference/src/components/fuse-highlight.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Fuse from 'fuse.js'
|
||||
import React from 'react'
|
||||
|
||||
function highlight(
|
||||
value: string,
|
||||
ranges: ReadonlyArray<Fuse.RangeTuple>,
|
||||
className: string,
|
||||
pos = ranges.length
|
||||
): React.ReactElement {
|
||||
const pair = ranges[pos - 1]
|
||||
|
||||
return pair ? (
|
||||
<>
|
||||
{highlight(value.substring(0, pair[0]), ranges, className, pos - 1)}
|
||||
<span className={className}>
|
||||
{value.substring(pair[0], pair[1] + 1)}
|
||||
</span>
|
||||
{value.substring(pair[1] + 1)}
|
||||
</>
|
||||
) : (
|
||||
<span>{value}</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function FuseHighlight({
|
||||
matches,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
matches: ReadonlyArray<Fuse.FuseResultMatch>
|
||||
value: string
|
||||
className: string
|
||||
}): React.ReactElement {
|
||||
return highlight(value, matches![0].indices, className)
|
||||
}
|
242
packages/tl-reference/src/components/global-search-field.tsx
Normal file
242
packages/tl-reference/src/components/global-search-field.tsx
Normal file
|
@ -0,0 +1,242 @@
|
|||
import { graphql, Link, useStaticQuery } from 'gatsby'
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
import { ExtendedTlObject, GraphqlAllResponse } from '../types'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
import {
|
||||
Avatar,
|
||||
createStyles,
|
||||
Fade,
|
||||
fade,
|
||||
InputBase,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Popper,
|
||||
Theme,
|
||||
Button,
|
||||
ClickAwayListener,
|
||||
} from '@material-ui/core'
|
||||
import React from 'react'
|
||||
import { useLocalState } from '../hooks/use-local-state'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import blue from '@material-ui/core/colors/blue'
|
||||
import red from '@material-ui/core/colors/red'
|
||||
import yellow from '@material-ui/core/colors/yellow'
|
||||
|
||||
import UnionIcon from '@material-ui/icons/AccountTree'
|
||||
import ClassIcon from '@material-ui/icons/Class'
|
||||
import FunctionsIcon from '@material-ui/icons/Functions'
|
||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
||||
import { useFuse } from '../hooks/use-fuse'
|
||||
import { FuseHighlight } from './fuse-highlight'
|
||||
import { ActionBarSearchField } from './actionbar-search-field'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
popup: {
|
||||
display: 'flex',
|
||||
height: 250,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
popupEmpty: {
|
||||
padding: theme.spacing(2),
|
||||
flex: '1 1 auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
popupEmptyIcon: {
|
||||
fontSize: '48px',
|
||||
display: 'block',
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
popupList: {
|
||||
width: '100%',
|
||||
},
|
||||
popupListItem: {
|
||||
padding: theme.spacing(0, 2),
|
||||
},
|
||||
searchItemMatch: {
|
||||
color: theme.palette.type === 'dark' ? blue[300] : blue[700],
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export function GlobalSearchField({ isMobile }: { isMobile: boolean }): React.ReactElement {
|
||||
const classes = useStyles()
|
||||
const allObjects: {
|
||||
allTlObject: GraphqlAllResponse<ExtendedTlObject>
|
||||
} = useStaticQuery(graphql`
|
||||
query {
|
||||
allTlObject {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const [includeMtproto, setIncludeMtproto] = useLocalState('mtproto', false)
|
||||
const { hits, query, onSearch } = useFuse(
|
||||
allObjects.allTlObject.edges,
|
||||
{
|
||||
keys: ['node.name'],
|
||||
includeMatches: true,
|
||||
threshold: 0.3,
|
||||
},
|
||||
{ limit: 25 },
|
||||
includeMtproto ? undefined : (it) => it.node.prefix !== 'mtproto/'
|
||||
)
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const notFound = () => (
|
||||
<>
|
||||
<ErrorOutlineIcon className={classes.popupEmptyIcon} />
|
||||
Nothing found
|
||||
{!includeMtproto && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
style={{
|
||||
margin: '4px auto',
|
||||
}}
|
||||
onClick={() => {
|
||||
setIncludeMtproto(true)
|
||||
}}
|
||||
>
|
||||
Retry including MTProto objects
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const emptyField = () => (
|
||||
<>
|
||||
<SearchIcon className={classes.popupEmptyIcon} />
|
||||
Start typing...
|
||||
</>
|
||||
)
|
||||
|
||||
const renderSearchItem = (
|
||||
node: ExtendedTlObject,
|
||||
matches: ReadonlyArray<Fuse.FuseResultMatch>
|
||||
) => (
|
||||
<ListItem
|
||||
button
|
||||
divider
|
||||
component={Link}
|
||||
to={`/${node.prefix}${node.type}/${node.name}`}
|
||||
className={classes.popupListItem}
|
||||
onClick={() => setOpen(false)}
|
||||
key={node.id}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
style={{
|
||||
backgroundColor:
|
||||
node.type === 'class'
|
||||
? blue[600]
|
||||
: node.type === 'method'
|
||||
? red[600]
|
||||
: yellow[700],
|
||||
}}
|
||||
>
|
||||
{node.type === 'class' ? (
|
||||
<ClassIcon />
|
||||
) : node.type === 'method' ? (
|
||||
<FunctionsIcon />
|
||||
) : (
|
||||
<UnionIcon />
|
||||
)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<>
|
||||
{node.prefix}
|
||||
<FuseHighlight
|
||||
matches={matches}
|
||||
value={node.name}
|
||||
className={classes.searchItemMatch}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
secondary={node.type}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
|
||||
const popupContent = (
|
||||
<Paper className={classes.popup}>
|
||||
{query.length <= 1 || !hits.length ? (
|
||||
<div className={classes.popupEmpty}>
|
||||
{query.length <= 1 ? emptyField() : notFound()}
|
||||
</div>
|
||||
) : (
|
||||
<List disablePadding dense className={classes.popupList}>
|
||||
{hits.map(({ item: { node }, matches }) =>
|
||||
renderSearchItem(node, matches!)
|
||||
)}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
style={{
|
||||
margin: '4px auto',
|
||||
}}
|
||||
onClick={() => {
|
||||
setIncludeMtproto(!includeMtproto)
|
||||
}}
|
||||
>
|
||||
{includeMtproto ? 'Hide' : 'Include'} MTProto
|
||||
objects
|
||||
</Button>
|
||||
</div>
|
||||
</List>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={() => setOpen(false)}>
|
||||
<>
|
||||
<ActionBarSearchField
|
||||
inputRef={setAnchorEl}
|
||||
autoComplete="off"
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={() => setOpen(false)}
|
||||
onChange={onSearch}
|
||||
/>
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
placement="bottom"
|
||||
transition
|
||||
style={{
|
||||
width: isMobile ? '100%' : anchorEl?.clientWidth,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade {...TransitionProps} timeout={350}>
|
||||
{popupContent}
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
}
|
67
packages/tl-reference/src/components/objects/link-to-tl.tsx
Normal file
67
packages/tl-reference/src/components/objects/link-to-tl.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Link as MuiLink } from '@material-ui/core'
|
||||
import { Link } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { ExtendedTlObject } from '../../types'
|
||||
|
||||
|
||||
export function LinkToTl(name: string): React.ReactElement
|
||||
export function LinkToTl(obj: ExtendedTlObject): React.ReactElement
|
||||
export function LinkToTl(
|
||||
prefix: string,
|
||||
type: string,
|
||||
name: string
|
||||
): React.ReactElement
|
||||
export function LinkToTl(
|
||||
prefix: string | ExtendedTlObject,
|
||||
type?: string,
|
||||
name?: string
|
||||
): React.ReactElement {
|
||||
if (typeof prefix !== 'string') {
|
||||
type = prefix.type
|
||||
name = prefix.name
|
||||
prefix = prefix.prefix
|
||||
}
|
||||
|
||||
// this kind of invokation is used in parameters table
|
||||
if (!type && !name) {
|
||||
const fullType = prefix
|
||||
|
||||
// core types
|
||||
if (
|
||||
fullType === 'number' ||
|
||||
fullType === 'Long' ||
|
||||
fullType === 'Int128' ||
|
||||
fullType === 'Int256' ||
|
||||
fullType === 'Double' ||
|
||||
fullType === 'string' ||
|
||||
fullType === 'Buffer' ||
|
||||
fullType === 'boolean' ||
|
||||
fullType === 'true' ||
|
||||
fullType === 'any' ||
|
||||
fullType === '$FlagsBitField'
|
||||
) {
|
||||
return (
|
||||
<MuiLink component={Link} to="/#core-types">
|
||||
{fullType === '$FlagsBitField' ? 'TlFlags' : fullType}
|
||||
</MuiLink>
|
||||
)
|
||||
}
|
||||
|
||||
// array
|
||||
if (fullType.substr(fullType.length - 2) === '[]') {
|
||||
return <>{LinkToTl(fullType.substr(0, fullType.length - 2))}[]</>
|
||||
}
|
||||
|
||||
// must be union since this is from parameters type
|
||||
prefix = ''
|
||||
type = 'union'
|
||||
name = fullType
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiLink component={Link} to={`/${prefix}${type}/${name}`}>
|
||||
{prefix}
|
||||
{name}
|
||||
</MuiLink>
|
||||
)
|
||||
}
|
183
packages/tl-reference/src/components/page.tsx
Normal file
183
packages/tl-reference/src/components/page.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
import { TableOfContents, TableOfContentsItem } from './table-of-contents'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
createStyles,
|
||||
Divider,
|
||||
Link as MuiLink,
|
||||
List,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Typography,
|
||||
} from '@material-ui/core'
|
||||
import { ExtendedTlObject } from '../types'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
container: {
|
||||
height: '100%',
|
||||
padding: 16,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
inner: {
|
||||
flex: '1 1 auto',
|
||||
},
|
||||
box: {
|
||||
paddingBottom: 32,
|
||||
},
|
||||
footer: {
|
||||
marginTop: 64,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export const usePageStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
heading0: {
|
||||
margin: theme.spacing(4, 0),
|
||||
},
|
||||
heading1: {
|
||||
marginBottom: theme.spacing(4),
|
||||
},
|
||||
heading: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(4),
|
||||
},
|
||||
paragraph: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export function Page({
|
||||
toc,
|
||||
children,
|
||||
}: {
|
||||
toc?: TableOfContentsItem[]
|
||||
children: React.ReactNode
|
||||
}): React.ReactElement {
|
||||
const classes = useStyles()
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||
|
||||
return (
|
||||
<Paper ref={setContainer} elevation={0} className={classes.container}>
|
||||
<Container maxWidth="md" className={classes.inner}>
|
||||
<Box className={classes.box}>
|
||||
{children}
|
||||
|
||||
<footer>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
variant="body2"
|
||||
className={classes.footer}
|
||||
>
|
||||
© MTCute TL reference. This website is{' '}
|
||||
<MuiLink href="https://github.com/teidesu/mtcute/tree/master/packages/tl-reference">
|
||||
open-source
|
||||
</MuiLink>{' '}
|
||||
and licensed under MIT.<br/>
|
||||
This website is not affiliated with Telegram.
|
||||
</Typography>
|
||||
</footer>
|
||||
</Box>
|
||||
</Container>
|
||||
{toc && <TableOfContents items={toc} container={container} />}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export function Description(params: {
|
||||
description: string | null
|
||||
component?: any
|
||||
className?: string
|
||||
}) {
|
||||
const { description, component: Component, ...other } = params as any
|
||||
|
||||
return Component ? (
|
||||
<Component
|
||||
{...other}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: description || 'No description available :(',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
{...other}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: description || 'No description available :(',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListItemTlObject({ node }: { node: ExtendedTlObject }) {
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: '16px 32px' }}>
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/${node.prefix}${node.type}/${node.name}`}
|
||||
>
|
||||
<Typography variant="h5" color="textPrimary">
|
||||
{node.prefix}
|
||||
{node.name}
|
||||
</Typography>
|
||||
</MuiLink>
|
||||
<Description description={node.description} />
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Section({
|
||||
title,
|
||||
id,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
id?: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const pageClasses = usePageStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && id && (
|
||||
<Typography
|
||||
variant="h4"
|
||||
id={id}
|
||||
className={pageClasses.heading}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function SectionWithList({
|
||||
nodes,
|
||||
children,
|
||||
...params
|
||||
}: Omit<Parameters<typeof Section>[0], 'children'> & {
|
||||
children?: React.ReactNode
|
||||
nodes: ExtendedTlObject[]
|
||||
}) {
|
||||
return (
|
||||
<Section {...params}>
|
||||
{children && <Typography variant="body1">{children}</Typography>}
|
||||
<List>
|
||||
{nodes.map((node) => (
|
||||
<ListItemTlObject node={node} key={node.id} />
|
||||
))}
|
||||
</List>
|
||||
</Section>
|
||||
)
|
||||
}
|
5
packages/tl-reference/src/components/spacer.tsx
Normal file
5
packages/tl-reference/src/components/spacer.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { styled } from '@material-ui/core'
|
||||
|
||||
export const Spacer = styled('div')({
|
||||
flex: '1 1 auto'
|
||||
})
|
234
packages/tl-reference/src/components/table-of-contents.tsx
Normal file
234
packages/tl-reference/src/components/table-of-contents.tsx
Normal file
|
@ -0,0 +1,234 @@
|
|||
import { createStyles, Link, makeStyles, Typography } from '@material-ui/core'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import throttle from 'lodash/throttle'
|
||||
import React, { MouseEvent } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// based on https://github.com/mui-org/material-ui/blob/master/docs/src/modules/components/AppTableOfContents.js
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
top: 80,
|
||||
// Fix IE 11 position sticky issue.
|
||||
width: 175,
|
||||
flexShrink: 0,
|
||||
order: 2,
|
||||
position: 'sticky',
|
||||
height: 'calc(100vh - 80px)',
|
||||
overflowY: 'auto',
|
||||
padding: theme.spacing(2, 2, 2, 0),
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
contents: {
|
||||
marginTop: theme.spacing(2),
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
},
|
||||
ul: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
listStyleType: 'none',
|
||||
},
|
||||
item: {
|
||||
fontSize: 13,
|
||||
padding: theme.spacing(0.5, 0, 0.5, 1),
|
||||
borderLeft: '4px solid transparent',
|
||||
boxSizing: 'content-box',
|
||||
'&:hover': {
|
||||
borderLeft: `4px solid ${
|
||||
theme.palette.type === 'light'
|
||||
? theme.palette.grey[200]
|
||||
: theme.palette.grey[900]
|
||||
}`,
|
||||
},
|
||||
'&$active,&:active': {
|
||||
borderLeft: `4px solid ${
|
||||
theme.palette.type === 'light'
|
||||
? theme.palette.grey[300]
|
||||
: theme.palette.grey[800]
|
||||
}`,
|
||||
},
|
||||
},
|
||||
secondaryItem: {
|
||||
paddingLeft: theme.spacing(2.5),
|
||||
},
|
||||
active: {},
|
||||
})
|
||||
)
|
||||
const noop = () => {}
|
||||
|
||||
function useThrottledOnScroll(
|
||||
callback: ((evt: Event) => void) | null,
|
||||
delay: number,
|
||||
container: HTMLElement | null
|
||||
) {
|
||||
const throttledCallback = useMemo(
|
||||
() => (callback ? throttle(callback, delay) : noop),
|
||||
[callback, delay]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (throttledCallback === noop || container === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', throttledCallback)
|
||||
return () => {
|
||||
container.removeEventListener('scroll', throttledCallback)
|
||||
}
|
||||
}, [throttledCallback, container])
|
||||
}
|
||||
|
||||
export interface TableOfContentsItem {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface TocWithNode extends TableOfContentsItem {
|
||||
node: HTMLElement
|
||||
}
|
||||
|
||||
export function TableOfContents({
|
||||
items,
|
||||
container,
|
||||
}: {
|
||||
items: TableOfContentsItem[]
|
||||
container: HTMLElement | null
|
||||
}): React.ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
const itemsWithNodeRef = useRef<TocWithNode[]>([])
|
||||
useEffect(() => {
|
||||
itemsWithNodeRef.current = items
|
||||
? items.map(({ id, title }) => {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
node: document.getElementById(id)!,
|
||||
}
|
||||
})
|
||||
: []
|
||||
}, [items])
|
||||
|
||||
const [activeState, setActiveState] = useState<string | null>(items[0]?.id || null)
|
||||
const clickedRef = useRef(false)
|
||||
const unsetClickedRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const findActiveIndex = useCallback(() => {
|
||||
// Don't set the active index based on scroll if a link was just clicked
|
||||
if (clickedRef.current || !container) {
|
||||
return
|
||||
}
|
||||
|
||||
let active
|
||||
for (let i = itemsWithNodeRef.current.length - 1; i >= 0; i -= 1) {
|
||||
const item = itemsWithNodeRef.current[i]
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!item.node) {
|
||||
console.error(
|
||||
`Missing node on the item ${JSON.stringify(
|
||||
item,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
item.node &&
|
||||
item.node.offsetTop <
|
||||
container.scrollTop + container.clientHeight / 8
|
||||
) {
|
||||
active = item
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (active && activeState !== active.id) {
|
||||
setActiveState(active.id!)
|
||||
}
|
||||
}, [activeState, container])
|
||||
|
||||
// Corresponds to 10 frames at 60 Hz
|
||||
useThrottledOnScroll(
|
||||
items && items.length > 0 ? findActiveIndex : null,
|
||||
166,
|
||||
container
|
||||
)
|
||||
|
||||
const handleClick = (id: string) => (
|
||||
event: MouseEvent<HTMLAnchorElement>
|
||||
) => {
|
||||
// Ignore click for new tab/new window behavior
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 || // ignore everything but left-click
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.altKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Used to disable findActiveIndex if the page scrolls due to a click
|
||||
clickedRef.current = true
|
||||
unsetClickedRef.current = setTimeout(() => {
|
||||
clickedRef.current = false
|
||||
}, 1000)
|
||||
|
||||
if (activeState !== id) {
|
||||
setActiveState(id)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
findActiveIndex()
|
||||
|
||||
return () => {
|
||||
if (unsetClickedRef.current) {
|
||||
clearTimeout(unsetClickedRef.current)
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const itemLink = (item: TableOfContentsItem): React.ReactElement => (
|
||||
<Link
|
||||
display="block"
|
||||
color={activeState === item.id ? 'textPrimary' : 'textSecondary'}
|
||||
href={`#${item.id}`}
|
||||
underline="none"
|
||||
onClick={handleClick(item.id)}
|
||||
className={clsx(
|
||||
classes.item,
|
||||
activeState === item.id ? classes.active : undefined
|
||||
)}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: item.title }} />
|
||||
</Link>
|
||||
)
|
||||
|
||||
return (
|
||||
<nav className={classes.root}>
|
||||
{items && items.length > 0 ? (
|
||||
<>
|
||||
<Typography gutterBottom className={classes.contents}>
|
||||
Contents
|
||||
</Typography>
|
||||
<Typography component="ul" className={classes.ul}>
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>{itemLink(item)}</li>
|
||||
))}
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
</nav>
|
||||
)
|
||||
}
|
48
packages/tl-reference/src/global.scss
Normal file
48
packages/tl-reference/src/global.scss
Normal file
|
@ -0,0 +1,48 @@
|
|||
html, body, #___gatsby, #gatsby-focus-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
html:not(.touch) *:not(.default-scroll) {
|
||||
scrollbar-color: #7f7f7f transparent;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #7f7f7f;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 80px offset for scroll anchors
|
||||
section, .is-anchor {
|
||||
margin-top: -80px;
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
h2, h3, h4 {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
table {
|
||||
overflow-x: auto;
|
||||
display: block!important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
41
packages/tl-reference/src/hooks/use-fuse.ts
Normal file
41
packages/tl-reference/src/hooks/use-fuse.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Fuse from 'fuse.js'
|
||||
import { ChangeEvent, useCallback, useMemo, useState } from 'react'
|
||||
import { debounce } from '@material-ui/core'
|
||||
|
||||
export function useFuse<T>(
|
||||
items: T[],
|
||||
options: Fuse.IFuseOptions<T>,
|
||||
searchOptions: Fuse.FuseSearchOptions,
|
||||
customFilter?: (it: T) => boolean
|
||||
) {
|
||||
const [query, updateQuery] = useState('')
|
||||
|
||||
const fuse = useMemo(() => new Fuse(items, options), [items, options])
|
||||
|
||||
const hits = useMemo(() => {
|
||||
if (!query) return []
|
||||
let res = fuse.search(query, searchOptions)
|
||||
if (customFilter) res = res.filter((it) => customFilter(it.item))
|
||||
return res
|
||||
}, [
|
||||
fuse,
|
||||
query,
|
||||
options,
|
||||
searchOptions,
|
||||
customFilter
|
||||
])
|
||||
|
||||
const setQuery = useCallback(debounce(updateQuery, 100), [])
|
||||
|
||||
const onSearch = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),
|
||||
[]
|
||||
)
|
||||
|
||||
return {
|
||||
hits,
|
||||
onSearch,
|
||||
query,
|
||||
setQuery
|
||||
}
|
||||
}
|
14
packages/tl-reference/src/hooks/use-local-state.ts
Normal file
14
packages/tl-reference/src/hooks/use-local-state.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
export function useLocalState<T>(key: string, init: T): [T, (val: T) => void] {
|
||||
const local = typeof localStorage !== 'undefined' ? localStorage[key] : undefined
|
||||
const [item, setItem] = useState<T>(local ? JSON.parse(local) : init)
|
||||
|
||||
return [
|
||||
item,
|
||||
(val: T) => {
|
||||
setItem(val)
|
||||
localStorage[key] = JSON.stringify(val)
|
||||
}
|
||||
]
|
||||
}
|
BIN
packages/tl-reference/src/images/icon.png
Normal file
BIN
packages/tl-reference/src/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
210
packages/tl-reference/src/layout.tsx
Normal file
210
packages/tl-reference/src/layout.tsx
Normal file
|
@ -0,0 +1,210 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
createMuiTheme, createStyles,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText, makeStyles,
|
||||
MuiThemeProvider,
|
||||
SwipeableDrawer,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
} from '@material-ui/core'
|
||||
import { Link } from 'gatsby'
|
||||
import { Spacer } from './components/spacer'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
import NightsStayIcon from '@material-ui/icons/NightsStay'
|
||||
import Brightness7Icon from '@material-ui/icons/Brightness7'
|
||||
import MenuIcon from '@material-ui/icons/Menu'
|
||||
|
||||
import './global.scss'
|
||||
import { GlobalSearchField } from './components/global-search-field'
|
||||
import blue from '@material-ui/core/colors/blue'
|
||||
import { isTouchDevice } from './utils'
|
||||
|
||||
const pages = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'About',
|
||||
regex: /^(?:\/tl|\/)\/?$/
|
||||
},
|
||||
{
|
||||
path: '/types',
|
||||
name: 'Types',
|
||||
regex: /^(?:\/tl)?(?:\/mtproto)?\/(class|union|types)(\/|$)/,
|
||||
},
|
||||
{
|
||||
path: '/methods',
|
||||
name: 'Methods',
|
||||
regex: /^(?:\/tl)?(?:\/mtproto)?\/methods?(\/|$)/,
|
||||
},
|
||||
// {
|
||||
// path: '/history',
|
||||
// name: 'History',
|
||||
// regex: /^\/history(\/|$)/,
|
||||
// },
|
||||
]
|
||||
|
||||
const drawerWidth = 240
|
||||
const useStyles = makeStyles((t) => createStyles({
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
},
|
||||
drawerPaper: {
|
||||
width: drawerWidth,
|
||||
},
|
||||
drawerItem: {
|
||||
padding: t.spacing(2, 4),
|
||||
fontSize: 18
|
||||
}
|
||||
}))
|
||||
|
||||
function MobileNavigation({ path }: { path: string }) {
|
||||
const [drawer, setDrawer] = useState(false)
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={() => setDrawer(true)}
|
||||
edge="start"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<SwipeableDrawer
|
||||
onClose={() => setDrawer(false)}
|
||||
onOpen={() => setDrawer(true)}
|
||||
open={drawer}
|
||||
className={classes.drawer}
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
{pages.map((page) => (
|
||||
<ListItem
|
||||
button
|
||||
component={Link}
|
||||
to={page.path}
|
||||
selected={
|
||||
page.regex
|
||||
? !!path.match(page.regex)
|
||||
: path === page.path
|
||||
}
|
||||
className={classes.drawerItem}
|
||||
key={page.name}
|
||||
>
|
||||
<ListItemText primary={page.name} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</SwipeableDrawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopNavigation({ path }: { path: string }) {
|
||||
return (
|
||||
<>
|
||||
{pages.map((page) => (
|
||||
<span
|
||||
style={{
|
||||
color: (
|
||||
page.regex
|
||||
? path.match(page.regex)
|
||||
: path === page.path
|
||||
)
|
||||
? '#fff'
|
||||
: '#ccc',
|
||||
}}
|
||||
key={page.path}
|
||||
>
|
||||
<Button color="inherit" component={Link} to={page.path}>
|
||||
{page.name}
|
||||
</Button>
|
||||
</span>
|
||||
))}
|
||||
<Spacer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ({
|
||||
children,
|
||||
location,
|
||||
}: {
|
||||
children: NonNullable<React.ReactNode>
|
||||
location: any
|
||||
}): React.ReactElement {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||
const path: string = location.pathname
|
||||
|
||||
useEffect(() => {
|
||||
if (isTouchDevice()) document.documentElement.classList.add('touch')
|
||||
}, [])
|
||||
|
||||
const muiTheme = createMuiTheme({
|
||||
palette: {
|
||||
type: theme,
|
||||
primary:
|
||||
theme === 'dark'
|
||||
? {
|
||||
main: blue[300],
|
||||
}
|
||||
: undefined,
|
||||
secondary: {
|
||||
main: blue[800],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const isDesktop = useMediaQuery(muiTheme.breakpoints.up('sm'))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet
|
||||
titleTemplate="%s | TL Reference"
|
||||
defaultTitle="TL Reference"
|
||||
/>
|
||||
<MuiThemeProvider theme={muiTheme}>
|
||||
<>
|
||||
<AppBar position="static" color="secondary">
|
||||
<Toolbar>
|
||||
{isDesktop ? (
|
||||
<DesktopNavigation path={path} />
|
||||
) : (
|
||||
<MobileNavigation path={path} />
|
||||
)}
|
||||
<GlobalSearchField isMobile={!isDesktop} />
|
||||
<Tooltip title="Toggle dark theme">
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={() =>
|
||||
setTheme(
|
||||
theme === 'dark' ? 'light' : 'dark'
|
||||
)
|
||||
}
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<NightsStayIcon />
|
||||
) : (
|
||||
<Brightness7Icon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
{children}
|
||||
</>
|
||||
</MuiThemeProvider>
|
||||
</>
|
||||
)
|
||||
}
|
27
packages/tl-reference/src/pages/404.tsx
Normal file
27
packages/tl-reference/src/pages/404.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { Page, usePageStyles } from '../components/page'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
const NotFoundPage = () => {
|
||||
const classes = usePageStyles()
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Helmet>
|
||||
<title>404 Not found</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
</Helmet>
|
||||
<div className={classes.heading1}>
|
||||
<Typography variant="h3" id="tl-reference">
|
||||
404
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
This page does not exist
|
||||
</Typography>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
20
packages/tl-reference/src/pages/history.tsx
Normal file
20
packages/tl-reference/src/pages/history.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react'
|
||||
import { Page, usePageStyles } from '../components/page'
|
||||
import { Typography } from '@material-ui/core'
|
||||
|
||||
export default function HistoryPage() {
|
||||
const classes = usePageStyles()
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className={classes.heading1}>
|
||||
<Typography variant="h3" id="tl-reference">
|
||||
History
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
This page is currently under construction
|
||||
</Typography>
|
||||
</Page>
|
||||
)
|
||||
}
|
367
packages/tl-reference/src/pages/index.tsx
Normal file
367
packages/tl-reference/src/pages/index.tsx
Normal file
|
@ -0,0 +1,367 @@
|
|||
import React from 'react'
|
||||
import { Link as MuiLink, Typography } from '@material-ui/core'
|
||||
import { Page, usePageStyles } from '../components/page'
|
||||
import { graphql, Link } from 'gatsby'
|
||||
|
||||
interface Data {
|
||||
mtClasses: { totalCount: number }
|
||||
classes: { totalCount: number }
|
||||
mtMethods: { totalCount: number }
|
||||
methods: { totalCount: number }
|
||||
mtUnions: { totalCount: number }
|
||||
unions: { totalCount: number }
|
||||
|
||||
clsWithDesc: { totalCount: number }
|
||||
clsWithoutDesc: { totalCount: number }
|
||||
argWithDesc: {
|
||||
totalCount: number
|
||||
nodes: { arguments: { description: string | null }[] }[]
|
||||
}
|
||||
argWithoutDesc: {
|
||||
totalCount: number
|
||||
nodes: { arguments: { description: string | null }[] }[]
|
||||
}
|
||||
|
||||
updated: {
|
||||
nodes: [
|
||||
{
|
||||
layer: number
|
||||
source: {
|
||||
date: string
|
||||
commit: string
|
||||
file: string
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function countMissingDescriptionArguments(
|
||||
item: Data['argWithDesc'],
|
||||
eqNull: boolean
|
||||
) {
|
||||
let count = 0
|
||||
item.nodes.forEach((node) =>
|
||||
node.arguments?.forEach((arg) => {
|
||||
if (eqNull ? arg.description === null : arg.description !== null)
|
||||
count += 1
|
||||
})
|
||||
)
|
||||
item.totalCount = count
|
||||
}
|
||||
|
||||
export default function IndexPage({ data }: { data: Data }) {
|
||||
const classes = usePageStyles()
|
||||
|
||||
countMissingDescriptionArguments(data.argWithoutDesc, true)
|
||||
countMissingDescriptionArguments(data.argWithDesc, false)
|
||||
|
||||
return (
|
||||
<Page
|
||||
toc={[
|
||||
{ id: 'tl-reference', title: 'TL Reference' },
|
||||
{ id: 'types', title: 'Types' },
|
||||
{ id: 'core-types', title: 'Core types' },
|
||||
{ id: 'statistics', title: 'Statistics' },
|
||||
]}
|
||||
>
|
||||
<div className={classes.heading1}>
|
||||
<Typography variant="h3" id="tl-reference">
|
||||
TL Reference
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
layer {data.updated.nodes[0].layer} / updated{' '}
|
||||
{data.updated.nodes[0].source.date}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
This web application allows easily browsing through myriads of
|
||||
TL objects and reading through their documentation. Unlike{' '}
|
||||
<MuiLink href="//core.telegram.org/schema">
|
||||
official documentation
|
||||
</MuiLink>
|
||||
, this app has simpler structure, search and nice interface.
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
Even though this reference is intended to be used with{' '}
|
||||
<MuiLink href="//github.com/teidesu/mtcute">MTCute</MuiLink>{' '}
|
||||
library, the objects are common to any other MTProto library.
|
||||
The key difference is that MTCute (and this reference) use{' '}
|
||||
<code>camelCase</code> for arguments, while the original schema
|
||||
and some other libraries use <code>snake_case</code>.
|
||||
</Typography>
|
||||
<Typography variant="h4" id="types" className={classes.heading}>
|
||||
Types
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
In TL, there are 3 main groups of types: <i>Classes</i>,{' '}
|
||||
<i>Methods</i> and Unions (officially they are called{' '}
|
||||
<i>constructors</i>, <i>methods</i> and <i>types</i>{' '}
|
||||
respectively).
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
<i>Classes</i> and <i>Methods</i> are simply typed objects, that
|
||||
contain some data. The only difference is that Methods are used
|
||||
in RPC calls (i.e. they are sent to the server), and Classes are
|
||||
used inside methods, or sent by the server back (either as an
|
||||
RPC result, or as an update).
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
<i>Union</i> is a type that combines multiple <i>Classes</i> in
|
||||
one type. In some languages, this can be represented as an
|
||||
abstract class. <i>Unions</i> are sent by Telegram in response
|
||||
to RPC results, as well as they are used as arguments for other{' '}
|
||||
<i>Classes</i> or <i>Methods</i>.
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
In TL, every single <i>Class</i> is a part of exactly one{' '}
|
||||
<i>Union</i>, and every <i>Union</i> contains at least one{' '}
|
||||
<i>Class</i>.
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
In MTCute, all types are exposed as a namespace <code>tl</code>{' '}
|
||||
of package <code>@mtcute/tl</code>. By design, we use immutable
|
||||
plain objects with type discriminator to represent{' '}
|
||||
<i>Classes</i> and <i>Methods</i>, and TypeScript unions to
|
||||
represent <i>Unions</i>.<br />
|
||||
To differentiate between different groups of types, we use
|
||||
different naming for each of them:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
className={classes.paragraph}
|
||||
component="ul"
|
||||
>
|
||||
<li>
|
||||
<i>Classes</i> are prefixed with <code>Raw</code> (e.g.{' '}
|
||||
<code>tl.RawMessage</code>)
|
||||
</li>
|
||||
<li>
|
||||
Additionally, <i>Methods</i> are postfixed with{' '}
|
||||
<code>Request</code> and (e.g.{' '}
|
||||
<code>tl.RawGetMessageRequest</code>)
|
||||
</li>
|
||||
<li>
|
||||
Finally, <i>Unions</i> are simply prefixed with{' '}
|
||||
<code>Type</code> (e.g. <code>tl.TypeUser</code>)
|
||||
</li>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h4"
|
||||
id="core-types"
|
||||
className={classes.heading}
|
||||
>
|
||||
Core types
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
Core types are basic built-in types that are used in TL schema.
|
||||
Quick reference:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
className={classes.paragraph}
|
||||
component="ul"
|
||||
>
|
||||
<li>
|
||||
<code>number</code>: 32-bit signed integer
|
||||
</li>
|
||||
<li>
|
||||
<code>Long</code>: 64-bit signed integer
|
||||
</li>
|
||||
<li>
|
||||
<code>Int128</code>: 128-bit signed integer (only used for
|
||||
MTProto)
|
||||
</li>
|
||||
<li>
|
||||
<code>Int256</code>: 256-bit signed integer (only used for
|
||||
MTProto)
|
||||
</li>
|
||||
<li>
|
||||
<code>Double</code>: 64-bit floating point value
|
||||
</li>
|
||||
<li>
|
||||
<code>string</code>: UTF-16 string (strings in JS are also
|
||||
UTF-16)
|
||||
</li>
|
||||
<li>
|
||||
<code>Buffer</code>: Byte array of a known size
|
||||
</li>
|
||||
<li>
|
||||
<code>boolean</code>: One-byte boolean value (true/false)
|
||||
</li>
|
||||
<li>
|
||||
<code>true</code>: Zero-size <code>true</code> value, used
|
||||
for TL flags
|
||||
</li>
|
||||
<li>
|
||||
<code>any</code>: Any other TL object (usually another
|
||||
method)
|
||||
</li>
|
||||
<li>
|
||||
<code>T[]</code>: Array of <code>T</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>TlFlags</code>: 32-bit signed value representing
|
||||
object's TL flags
|
||||
</li>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h4"
|
||||
className={classes.heading}
|
||||
id="statistics"
|
||||
>
|
||||
Statistics
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
className={classes.paragraph}
|
||||
component="ul"
|
||||
>
|
||||
<li>
|
||||
Generated from layer <b>{data.updated.nodes[0].layer}</b>{' '}
|
||||
(last updated <b>{data.updated.nodes[0].source.date}</b>,
|
||||
commit{' '}
|
||||
<MuiLink
|
||||
href={`https://github.com/telegramdesktop/tdesktop/blob/${data.updated.nodes[0].source.commit}/${data.updated.nodes[0].source.file}`}
|
||||
target="_blank"
|
||||
>
|
||||
{data.updated.nodes[0].source.commit.substr(0, 7)}
|
||||
</MuiLink>
|
||||
)
|
||||
</li>
|
||||
<li>
|
||||
Current schema contains{' '}
|
||||
<b>
|
||||
{data.methods.totalCount +
|
||||
data.classes.totalCount +
|
||||
data.unions.totalCount}
|
||||
</b>{' '}
|
||||
types (+{' '}
|
||||
<b>
|
||||
{data.mtClasses.totalCount +
|
||||
data.mtMethods.totalCount +
|
||||
data.mtUnions.totalCount}
|
||||
</b>{' '}
|
||||
for MTProto)
|
||||
</li>
|
||||
<li>
|
||||
Current schema contains <b>{data.classes.totalCount}</b>{' '}
|
||||
classes (+ <b>{data.mtClasses.totalCount}</b> for MTProto)
|
||||
</li>
|
||||
<li>
|
||||
Current schema contains <b>{data.methods.totalCount}</b>{' '}
|
||||
methods (+ <b>{data.mtMethods.totalCount}</b> for MTProto)
|
||||
</li>
|
||||
<li>
|
||||
Current schema contains <b>{data.unions.totalCount}</b>{' '}
|
||||
unions (+ <b>{data.mtUnions.totalCount}</b> for MTProto)
|
||||
</li>
|
||||
<li>
|
||||
Description coverage:{' '}
|
||||
{(function () {
|
||||
const totalWith =
|
||||
data.argWithDesc.totalCount +
|
||||
data.clsWithDesc.totalCount
|
||||
const totalWithout =
|
||||
data.argWithoutDesc.totalCount +
|
||||
data.clsWithoutDesc.totalCount
|
||||
const total = totalWith + totalWithout
|
||||
|
||||
return (
|
||||
<>
|
||||
<b>
|
||||
{Math.round((totalWith / total) * 10000) /
|
||||
100}
|
||||
%
|
||||
</b>{' '}
|
||||
(out of {total} items, {totalWithout}{' '}
|
||||
<MuiLink component={Link} to="/no-description">
|
||||
don't have description
|
||||
</MuiLink>{' '}
|
||||
- that is {data.clsWithoutDesc.totalCount} types
|
||||
and {data.argWithoutDesc.totalCount} arguments)
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</li>
|
||||
</Typography>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query {
|
||||
mtClasses: allTlObject(
|
||||
filter: { type: { eq: "class" }, prefix: { eq: "mtproto/" } }
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
classes: allTlObject(
|
||||
filter: { type: { eq: "class" }, prefix: { ne: "mtproto/" } }
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
mtMethods: allTlObject(
|
||||
filter: { type: { eq: "method" }, prefix: { eq: "mtproto/" } }
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
methods: allTlObject(
|
||||
filter: { type: { eq: "method" }, prefix: { ne: "mtproto/" } }
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
mtUnions: allTlObject(
|
||||
filter: { type: { eq: "union" }, prefix: { eq: "mtproto/" } }
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
unions: allTlObject(
|
||||
filter: { type: { eq: "union" }, prefix: { ne: "mtproto/" } }
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
|
||||
updated: allHistoryJson(
|
||||
sort: { fields: source___date, order: DESC }
|
||||
filter: { layer: { ne: null } }
|
||||
limit: 1
|
||||
) {
|
||||
nodes {
|
||||
layer
|
||||
source {
|
||||
date(formatString: "DD-MM-YYYY")
|
||||
commit
|
||||
file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clsWithDesc: allTlObject(filter: { description: { ne: null } }) {
|
||||
totalCount
|
||||
}
|
||||
clsWithoutDesc: allTlObject(filter: { description: { eq: null } }) {
|
||||
totalCount
|
||||
}
|
||||
argWithDesc: allTlObject(
|
||||
filter: { arguments: { elemMatch: { description: { ne: null } } } }
|
||||
) {
|
||||
totalCount
|
||||
nodes {
|
||||
arguments {
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
argWithoutDesc: allTlObject(
|
||||
filter: { arguments: { elemMatch: { description: { eq: null } } } }
|
||||
) {
|
||||
totalCount
|
||||
nodes {
|
||||
arguments {
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
178
packages/tl-reference/src/pages/no-description.tsx
Normal file
178
packages/tl-reference/src/pages/no-description.tsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { ExtendedTlObject } from '../types'
|
||||
import { Page, Section, usePageStyles } from '../components/page'
|
||||
import {
|
||||
Link as MuiLink,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@material-ui/core'
|
||||
import { LinkToTl } from '../components/objects/link-to-tl'
|
||||
|
||||
interface Data {
|
||||
classes: { nodes: ExtendedTlObject[] }
|
||||
arguments: { nodes: ExtendedTlObject[] }
|
||||
}
|
||||
|
||||
export default function NoDescriptionPage({ data }: { data: Data }) {
|
||||
const classes = usePageStyles()
|
||||
|
||||
// i dont care
|
||||
const args: any[] = []
|
||||
data.arguments.nodes.forEach((node) => {
|
||||
if (node.arguments) {
|
||||
node.arguments.forEach((arg) => {
|
||||
if (arg.description === null) {
|
||||
;(arg as any).node = node
|
||||
args.push(arg)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Page
|
||||
toc={[
|
||||
{ id: 'types', title: 'Types' },
|
||||
{ id: 'arguments', title: 'Arguments' },
|
||||
]}
|
||||
>
|
||||
<div className={classes.heading1}>
|
||||
<Typography variant="h3" id="tl-reference">
|
||||
No description
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{data.classes.nodes.length} types, {args.length} arguments
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="body1" className={classes.paragraph}>
|
||||
This page lists all items (types and their arguments) from the
|
||||
schema that currently do not have a description, neither
|
||||
official nor unofficial. You can improve this reference by
|
||||
adding description to missing items in{' '}
|
||||
<MuiLink href="https://github.com/teidesu/mtcute/blob/master/packages/tl/descriptions.yaml">
|
||||
descriptions.yaml
|
||||
</MuiLink>
|
||||
.
|
||||
</Typography>
|
||||
<Section id="types" title="Types">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>
|
||||
<MuiLink href="https://github.com/teidesu/mtcute/blob/master/packages/tl/descriptions.yaml">
|
||||
descriptions.yaml
|
||||
</MuiLink>{' '}
|
||||
key
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.classes.nodes.map((node) => (
|
||||
<TableRow key={node.id}>
|
||||
<TableCell>
|
||||
{node.type === 'method'
|
||||
? 'Method'
|
||||
: node.type === 'union'
|
||||
? 'Union'
|
||||
: 'Class'}
|
||||
</TableCell>
|
||||
<TableCell>{LinkToTl(node)}</TableCell>
|
||||
<TableCell>
|
||||
{(node.type === 'method' ? 'm_' : 'o_') +
|
||||
(node.prefix === 'mtproto/'
|
||||
? 'mt_'
|
||||
: '') +
|
||||
(node.type === 'union'
|
||||
? node.name
|
||||
: node.underscore)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
<Section id="arguments" title="Arguments">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>
|
||||
<MuiLink href="https://github.com/teidesu/mtcute/blob/master/packages/tl/descriptions.yaml">
|
||||
descriptions.yaml
|
||||
</MuiLink>{' '}
|
||||
key
|
||||
</TableCell>
|
||||
<TableCell>Argument</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{args.map((arg) => (
|
||||
<TableRow key={arg.node.id + arg.name}>
|
||||
<TableCell>
|
||||
{arg.node.type === 'method'
|
||||
? 'Method'
|
||||
: arg.node.type === 'union'
|
||||
? 'Union'
|
||||
: 'Class'}
|
||||
</TableCell>
|
||||
<TableCell>{LinkToTl(arg.node)}</TableCell>
|
||||
<TableCell>
|
||||
{(arg.node.type === 'method'
|
||||
? 'm_'
|
||||
: 'o_') +
|
||||
(arg.node.prefix === 'mtproto/'
|
||||
? 'mt_'
|
||||
: '') +
|
||||
(arg.node.type === 'union'
|
||||
? arg.node.name
|
||||
: arg.node.underscore)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code>{arg.name}</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query {
|
||||
classes: allTlObject(filter: { description: { eq: null } }) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
underscore
|
||||
}
|
||||
}
|
||||
arguments: allTlObject(
|
||||
filter: { arguments: { elemMatch: { description: { eq: null } } } }
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
underscore
|
||||
arguments {
|
||||
name
|
||||
type
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
486
packages/tl-reference/src/templates/tl-object.tsx
Normal file
486
packages/tl-reference/src/templates/tl-object.tsx
Normal file
|
@ -0,0 +1,486 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import { ExtendedTlObject } from '../types'
|
||||
import {
|
||||
Description,
|
||||
Page,
|
||||
Section,
|
||||
SectionWithList,
|
||||
usePageStyles,
|
||||
} from '../components/page'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core'
|
||||
import {
|
||||
createStyles,
|
||||
Link as MuiLink,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core'
|
||||
import { Link } from 'gatsby'
|
||||
import { LinkToTl } from '../components/objects/link-to-tl'
|
||||
import { TableOfContentsItem } from '../components/table-of-contents'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
interface GraphqlResult {
|
||||
self: ExtendedTlObject
|
||||
parent: ExtendedTlObject
|
||||
children: { nodes: ExtendedTlObject[] }
|
||||
usageMethods: { nodes: ExtendedTlObject[] }
|
||||
usageTypes: { nodes: ExtendedTlObject[] }
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
description: {
|
||||
marginBottom: theme.spacing(2),
|
||||
fontSize: 16,
|
||||
},
|
||||
table: {
|
||||
'& th, & td': {
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
mono: {
|
||||
fontFamily: 'Fira Mono, Consolas, monospace',
|
||||
},
|
||||
// theme ported from one dark
|
||||
code: {
|
||||
fontFamily: 'Fira Mono, Consolas, monospace',
|
||||
background: '#282c34',
|
||||
color: '#bbbbbb',
|
||||
fontSize: 16,
|
||||
borderRadius: 4,
|
||||
overflowX: 'auto',
|
||||
padding: 8,
|
||||
},
|
||||
keyword: {
|
||||
fontStyle: 'italic',
|
||||
color: '#c678dd',
|
||||
},
|
||||
identifier: {
|
||||
color: '#e5c07b',
|
||||
},
|
||||
property: {
|
||||
color: '#e06c75',
|
||||
},
|
||||
comment: {
|
||||
color: '#5c6370',
|
||||
},
|
||||
string: {
|
||||
color: '#98c379',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
function useToc(obj: ExtendedTlObject): TableOfContentsItem[] {
|
||||
return useMemo(() => {
|
||||
const ret = [{ id: 'title', title: obj.prefix + obj.name }]
|
||||
|
||||
if (obj.type !== 'union') {
|
||||
ret.push({ id: 'parameters', title: 'Parameters' })
|
||||
} else {
|
||||
ret.push({ id: 'subtypes', title: 'Subtypes' })
|
||||
ret.push({ id: 'usage', title: 'Usage' })
|
||||
}
|
||||
|
||||
if (obj.type === 'method' && obj.throws) {
|
||||
ret.push({ id: 'throws', title: 'Throws' })
|
||||
}
|
||||
|
||||
ret.push({ id: 'typescript', title: 'TypeScript' })
|
||||
|
||||
return ret
|
||||
}, [obj])
|
||||
}
|
||||
|
||||
export default function TlObject({ data }: { data: GraphqlResult }) {
|
||||
const pageClasses = usePageStyles()
|
||||
const classes = useStyles()
|
||||
|
||||
const obj = data.self
|
||||
const toc = useToc(obj)
|
||||
|
||||
const keyword = (s: string) =>
|
||||
`<span class='${classes.keyword}'>${s}</span>`
|
||||
const identifier = (s: string) =>
|
||||
`<span class='${classes.identifier}'>${s}</span>`
|
||||
const property = (s: string) =>
|
||||
`<span class='${classes.property}'>${s}</span>`
|
||||
const comment = (s: string) =>
|
||||
`<span class='${classes.comment}'>${s}</span>`
|
||||
const _string = (s: string) => `<span class='${classes.string}'>${s}</span>`
|
||||
|
||||
const typeName = (s: string): string => {
|
||||
if (
|
||||
s === 'string' ||
|
||||
s === 'number' ||
|
||||
s === 'boolean' ||
|
||||
s === 'true'
|
||||
) {
|
||||
return keyword(s)
|
||||
}
|
||||
|
||||
if (s.substr(s.length - 2) === '[]')
|
||||
return typeName(s.substr(0, s.length - 2)) + '[]'
|
||||
|
||||
return s.split('.').map(identifier).join('.')
|
||||
}
|
||||
|
||||
const code = (s: string) => {
|
||||
return (
|
||||
<pre
|
||||
className={classes.code}
|
||||
dangerouslySetInnerHTML={{ __html: s }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Page toc={toc}>
|
||||
<Helmet>
|
||||
<title>
|
||||
{obj.prefix}
|
||||
{obj.name}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={
|
||||
obj.description ||
|
||||
obj.prefix + obj.name + " currently doesn't have a description."
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
<div className={pageClasses.heading0}>
|
||||
<Breadcrumbs>
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/${obj.prefix}${
|
||||
obj.type === 'method' ? 'methods' : 'types'
|
||||
}`}
|
||||
>
|
||||
{obj.prefix}
|
||||
{obj.type === 'method' ? 'Methods' : 'Types'}
|
||||
</MuiLink>
|
||||
{obj.namespace !== '$root' && (
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/${obj.prefix}${
|
||||
obj.type === 'method' ? 'methods' : 'types'
|
||||
}/${obj.namespace}`}
|
||||
>
|
||||
{obj.prefix}
|
||||
{obj.namespace}
|
||||
</MuiLink>
|
||||
)}
|
||||
<Typography color="textPrimary">{obj.name}</Typography>
|
||||
</Breadcrumbs>
|
||||
<Typography variant="h3" id="title">
|
||||
{obj.prefix}
|
||||
{obj.name}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{obj.type === 'class' ? (
|
||||
<>
|
||||
constructor ID 0x{obj.tlId} / belongs to union{' '}
|
||||
{LinkToTl(data.parent)}
|
||||
</>
|
||||
) : obj.type === 'union' ? (
|
||||
<>
|
||||
has{' '}
|
||||
<MuiLink href="#subtypes">
|
||||
{data.children.nodes.length} sub-types
|
||||
</MuiLink>{' '}
|
||||
and{' '}
|
||||
<MuiLink href="#usage">
|
||||
{data.usageTypes.nodes.length +
|
||||
data.usageMethods.nodes.length}{' '}
|
||||
usages
|
||||
</MuiLink>
|
||||
</>
|
||||
) : (
|
||||
obj.returns && (
|
||||
<>
|
||||
constructor ID 0x{obj.tlId} / returns{' '}
|
||||
{LinkToTl(obj.prefix, 'union', obj.returns)}
|
||||
{obj.available &&
|
||||
' / available for ' +
|
||||
(obj.available === 'both'
|
||||
? 'both users and bots'
|
||||
: obj.available + 's only')}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<Description
|
||||
description={obj.description}
|
||||
className={classes.description}
|
||||
/>
|
||||
{obj.type !== 'union' && (
|
||||
<Section id="parameters" title="Parameters">
|
||||
<Table className={classes.table}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{obj.arguments.map((arg) => (
|
||||
<TableRow key={arg.name}>
|
||||
<TableCell>
|
||||
<code>{arg.name}</code>
|
||||
</TableCell>
|
||||
<TableCell className={classes.mono}>
|
||||
{LinkToTl(arg.type)}
|
||||
{arg.optional ? '?' : ''}
|
||||
</TableCell>
|
||||
<Description
|
||||
description={arg.description}
|
||||
component={TableCell}
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
{obj.type === 'union' && (
|
||||
<>
|
||||
<SectionWithList
|
||||
id="subtypes"
|
||||
title="Subtypes"
|
||||
nodes={data.children.nodes}
|
||||
>
|
||||
{obj.prefix}
|
||||
{obj.name} can be represented with{' '}
|
||||
{obj.subtypes.length > 1
|
||||
? `one of ${obj.subtypes.length} classes`
|
||||
: 'only one class'}
|
||||
:
|
||||
</SectionWithList>
|
||||
|
||||
<Section id="usage" title="Usage">
|
||||
{data.usageMethods.nodes.length > 0 && (
|
||||
<SectionWithList nodes={data.usageMethods.nodes}>
|
||||
{obj.prefix}
|
||||
{obj.name} is returned by{' '}
|
||||
{data.usageMethods.nodes.length > 1
|
||||
? `${data.usageMethods.nodes.length} methods`
|
||||
: 'only one method'}
|
||||
:
|
||||
</SectionWithList>
|
||||
)}
|
||||
|
||||
{data.usageTypes.nodes.length > 0 && (
|
||||
<SectionWithList nodes={data.usageTypes.nodes}>
|
||||
{obj.prefix}
|
||||
{obj.name} is used in{' '}
|
||||
{data.usageTypes.nodes.length > 1
|
||||
? `${data.usageTypes.nodes.length} types`
|
||||
: 'only one type'}
|
||||
:
|
||||
</SectionWithList>
|
||||
)}
|
||||
|
||||
{data.usageMethods.nodes.length === 0 &&
|
||||
data.usageTypes.nodes.length === 0 && (
|
||||
<Typography color="textSecondary">
|
||||
This union is never used :(
|
||||
</Typography>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
{obj.throws && (
|
||||
<Section id="throws" title="Throws">
|
||||
<Table className={classes.table}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Code</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{obj.throws.map((err) => (
|
||||
<TableRow key={err.name}>
|
||||
<TableCell>
|
||||
<code>{err.code}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code>{err.name}</code>
|
||||
</TableCell>
|
||||
<Description
|
||||
description={err.description}
|
||||
component={TableCell}
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
<Typography
|
||||
variant="h4"
|
||||
id="typescript"
|
||||
className={pageClasses.heading}
|
||||
>
|
||||
TypeScript declaration
|
||||
</Typography>
|
||||
|
||||
{/* this is a mess, but who cares */}
|
||||
{code(
|
||||
obj.type === 'union'
|
||||
? `${keyword('export type')} ${identifier(obj.ts)} =` +
|
||||
data.children.nodes
|
||||
.map(
|
||||
(it) =>
|
||||
`\n | ${typeName(
|
||||
'tl.' +
|
||||
(it.namespace === '$root'
|
||||
? it.prefix === 'mtproto/'
|
||||
? 'mtproto.'
|
||||
: ''
|
||||
: it.namespace + '.') +
|
||||
it.ts
|
||||
)}`
|
||||
)
|
||||
.join('')
|
||||
: `${keyword('export interface')} ${identifier(obj.ts)} {` +
|
||||
`\n ${property('_')}: ${_string(
|
||||
`'${obj.prefix === 'mtproto/' ? 'mt_' : ''}${
|
||||
obj.name
|
||||
}'`
|
||||
)}` +
|
||||
obj.arguments
|
||||
.map((arg) =>
|
||||
arg.type === '$FlagsBitField'
|
||||
? comment(
|
||||
'\n // ' +
|
||||
arg.name +
|
||||
': TlFlags // handled automatically'
|
||||
)
|
||||
: `\n ${property(arg.name)}${
|
||||
arg.optional ? '?' : ''
|
||||
}: ${typeName(arg.ts)}${
|
||||
arg.predicate
|
||||
? ' ' +
|
||||
comment(
|
||||
'// present if ' +
|
||||
arg.predicate
|
||||
)
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
.join('') +
|
||||
'\n}'
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query(
|
||||
$prefix: String!
|
||||
$type: String!
|
||||
$name: String!
|
||||
$hasSubtypes: Boolean!
|
||||
$subtypes: [String]
|
||||
) {
|
||||
self: tlObject(
|
||||
prefix: { eq: $prefix }
|
||||
type: { eq: $type }
|
||||
name: { eq: $name }
|
||||
) {
|
||||
tlId
|
||||
ts
|
||||
prefix
|
||||
type
|
||||
name
|
||||
description
|
||||
namespace
|
||||
returns
|
||||
available
|
||||
arguments {
|
||||
name
|
||||
ts
|
||||
type
|
||||
description
|
||||
optional
|
||||
predicate
|
||||
}
|
||||
subtypes
|
||||
throws {
|
||||
code
|
||||
description
|
||||
name
|
||||
}
|
||||
}
|
||||
parent: tlObject(
|
||||
prefix: { eq: $prefix }
|
||||
type: { eq: "union" }
|
||||
subtypes: { eq: $name }
|
||||
) {
|
||||
prefix
|
||||
name
|
||||
type
|
||||
description
|
||||
subtypes
|
||||
}
|
||||
children: allTlObject(
|
||||
filter: {
|
||||
prefix: { eq: $prefix }
|
||||
type: { eq: "class" }
|
||||
name: { in: $subtypes }
|
||||
}
|
||||
) @include(if: $hasSubtypes) {
|
||||
nodes {
|
||||
ts
|
||||
id
|
||||
namespace
|
||||
prefix
|
||||
name
|
||||
type
|
||||
description
|
||||
}
|
||||
}
|
||||
usageMethods: allTlObject(
|
||||
filter: {
|
||||
prefix: { eq: $prefix }
|
||||
type: { eq: "method" }
|
||||
returns: { eq: $name }
|
||||
}
|
||||
) @include(if: $hasSubtypes) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
usageTypes: allTlObject(
|
||||
filter: {
|
||||
prefix: { eq: $prefix }
|
||||
arguments: { elemMatch: { type: { eq: $name } } }
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
279
packages/tl-reference/src/templates/tl-types-list.tsx
Normal file
279
packages/tl-reference/src/templates/tl-types-list.tsx
Normal file
|
@ -0,0 +1,279 @@
|
|||
import { graphql, Link } from 'gatsby'
|
||||
import React, { useMemo } from 'react'
|
||||
import { ExtendedTlObject } from '../types'
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
SectionWithList,
|
||||
usePageStyles,
|
||||
} from '../components/page'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
createStyles,
|
||||
Link as MuiLink,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from '@material-ui/core'
|
||||
import { TableOfContentsItem } from '../components/table-of-contents'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
interface Data {
|
||||
classes: { nodes: ExtendedTlObject[] }
|
||||
unions: { nodes: ExtendedTlObject[] }
|
||||
methods: { nodes: ExtendedTlObject[] }
|
||||
other: { group: { fieldValue: string }[] }
|
||||
}
|
||||
|
||||
interface Context {
|
||||
ns: string
|
||||
prefix: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
namespace: {
|
||||
display: 'inline-block',
|
||||
margin: theme.spacing(0, 1),
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
function useToc(data: Data, ctx: Context): TableOfContentsItem[] {
|
||||
return useMemo(() => {
|
||||
const ret = [{ id: 'namespaces', title: 'Namespaces' }]
|
||||
|
||||
if (ctx.type === 'types') {
|
||||
ret.push({ id: 'classes', title: 'Classes' })
|
||||
ret.push({ id: 'unions', title: 'Unions' })
|
||||
} else {
|
||||
ret.push({ id: 'methods', title: 'Methods' })
|
||||
}
|
||||
|
||||
return ret
|
||||
}, [data, ctx])
|
||||
}
|
||||
|
||||
export default function TlTypesList({
|
||||
data,
|
||||
pageContext: ctx,
|
||||
}: {
|
||||
data: Data
|
||||
pageContext: Context
|
||||
}) {
|
||||
const pageClasses = usePageStyles()
|
||||
const classes = useStyles()
|
||||
|
||||
const toc = useToc(data, ctx)
|
||||
|
||||
const title = `${ctx.type === 'methods' ? 'Methods' : 'Types'}
|
||||
${
|
||||
ctx.ns === '$root' && ctx.prefix === ''
|
||||
? ''
|
||||
: ctx.ns === '$root'
|
||||
? `in ${ctx.prefix.slice(0, ctx.prefix.length - 1)}`
|
||||
: ` in ${ctx.prefix}${ctx.ns}`
|
||||
}`
|
||||
|
||||
const plural = (val: number, singular: string, postfix = 's') =>
|
||||
val + ' ' + (val === 1 ? singular : singular + postfix)
|
||||
|
||||
let description = ''
|
||||
{
|
||||
if (ctx.prefix === 'mtproto/') description = 'MTProto '
|
||||
if (ctx.ns === '$root') {
|
||||
description += 'TL Schema'
|
||||
} else {
|
||||
description += 'Namespace ' + ctx.ns
|
||||
}
|
||||
description += ' contains '
|
||||
if (ctx.ns === '$root' && data.other.group.length) {
|
||||
description += plural(data.other.group.length, 'namespace') + ', '
|
||||
}
|
||||
|
||||
if (ctx.type === 'methods') {
|
||||
description +=
|
||||
plural(data.methods.nodes.length, 'method') +
|
||||
', including: ' +
|
||||
data.methods.nodes
|
||||
.slice(0, 3)
|
||||
.map((i) => i.name)
|
||||
.join(', ')
|
||||
|
||||
if (data.methods.nodes.length > 3) description += ' and others.'
|
||||
} else {
|
||||
description +=
|
||||
plural(data.classes.nodes.length, 'class', 'es') +
|
||||
' and ' +
|
||||
plural(data.unions.nodes.length, 'union') +
|
||||
', including: ' +
|
||||
data.classes.nodes
|
||||
.slice(0, 3)
|
||||
.map((i) => i.name)
|
||||
.join(', ')
|
||||
|
||||
if (data.classes.nodes.length > 3) description += ' and others.'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page toc={toc}>
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</Helmet>
|
||||
|
||||
<div className={pageClasses.heading0}>
|
||||
<Breadcrumbs>
|
||||
{ctx.ns === '$root' ? (
|
||||
<Typography color="textPrimary">
|
||||
{ctx.prefix}{ctx.type === 'methods' ? 'Methods' : 'Types'}
|
||||
</Typography>
|
||||
) : (
|
||||
[
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/${ctx.prefix}${ctx.type}`}
|
||||
key="type"
|
||||
>
|
||||
{ctx.prefix}{ctx.type === 'methods' ? 'Methods' : 'Types'}
|
||||
</MuiLink>,
|
||||
<Typography color="textPrimary" key="namespace">
|
||||
{ctx.ns}
|
||||
</Typography>,
|
||||
]
|
||||
)}
|
||||
</Breadcrumbs>
|
||||
<Typography variant="h3" id="title">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{ctx.ns === '$root' &&
|
||||
`has ${plural(
|
||||
data.other.group.length,
|
||||
'namespace'
|
||||
)} / `}
|
||||
{ctx.type === 'methods'
|
||||
? `has ${plural(data.methods.nodes.length, 'method')}`
|
||||
: `has ${plural(
|
||||
data.classes.nodes.length,
|
||||
'class',
|
||||
'es'
|
||||
)}` +
|
||||
` and ${plural(data.unions.nodes.length, 'union')}`}
|
||||
</Typography>
|
||||
</div>
|
||||
{data.other.group.length && (
|
||||
<Section id="namespaces" title="Namespaces">
|
||||
{data.other.group.map(({ fieldValue: it }) =>
|
||||
it === ctx.ns ? (
|
||||
<Typography
|
||||
color="textPrimary"
|
||||
className={classes.namespace}
|
||||
key={it}
|
||||
>
|
||||
{ctx.ns === '$root' ? 'root' : ctx.ns}
|
||||
</Typography>
|
||||
) : (
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/${ctx.prefix}${ctx.type}${
|
||||
it === '$root' ? '' : '/' + it
|
||||
}`}
|
||||
className={classes.namespace}
|
||||
key={it}
|
||||
>
|
||||
{it === '$root' ? 'root' : it}
|
||||
</MuiLink>
|
||||
)
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
{ctx.type === 'types' && (
|
||||
<>
|
||||
<SectionWithList
|
||||
id="classes"
|
||||
title="Classes"
|
||||
nodes={data.classes.nodes}
|
||||
/>
|
||||
<SectionWithList
|
||||
id="unions"
|
||||
title="Unions"
|
||||
nodes={data.unions.nodes}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{ctx.type === 'methods' && (
|
||||
<SectionWithList
|
||||
id="methods"
|
||||
title="Methods"
|
||||
nodes={data.methods.nodes}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query(
|
||||
$ns: String!
|
||||
$prefix: String!
|
||||
$isTypes: Boolean!
|
||||
$isMethods: Boolean!
|
||||
) {
|
||||
classes: allTlObject(
|
||||
filter: {
|
||||
prefix: { eq: $prefix }
|
||||
namespace: { eq: $ns }
|
||||
type: { eq: "class" }
|
||||
}
|
||||
) @include(if: $isTypes) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
unions: allTlObject(
|
||||
filter: {
|
||||
prefix: { eq: $prefix }
|
||||
namespace: { eq: $ns }
|
||||
type: { eq: "union" }
|
||||
}
|
||||
) @include(if: $isTypes) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
methods: allTlObject(
|
||||
filter: {
|
||||
prefix: { eq: $prefix }
|
||||
namespace: { eq: $ns }
|
||||
type: { eq: "method" }
|
||||
}
|
||||
) @include(if: $isMethods) {
|
||||
nodes {
|
||||
id
|
||||
prefix
|
||||
type
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
other: allTlObject(filter: { prefix: { eq: $prefix } }) {
|
||||
group(field: namespace) {
|
||||
fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
33
packages/tl-reference/src/types.tsx
Normal file
33
packages/tl-reference/src/types.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
export interface ExtendedTlObject {
|
||||
id: string
|
||||
tlId: number
|
||||
ts: string
|
||||
prefix: string
|
||||
available: 'user' | 'bot' | 'both'
|
||||
type: 'union' | 'class' | 'method'
|
||||
name: string
|
||||
namespace: string
|
||||
returns: string
|
||||
underscore: string
|
||||
description: string | null
|
||||
arguments: {
|
||||
ts: string
|
||||
optional?: boolean
|
||||
name: string
|
||||
type: string
|
||||
predicate: string
|
||||
description: string | null
|
||||
}[]
|
||||
throws: {
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
}[]
|
||||
subtypes: string[]
|
||||
}
|
||||
|
||||
export interface GraphqlAllResponse<T> {
|
||||
edges: {
|
||||
node: T
|
||||
}[]
|
||||
}
|
20
packages/tl-reference/src/utils.ts
Normal file
20
packages/tl-reference/src/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export const isTouchDevice = function (): boolean {
|
||||
// because windows touch support stinks
|
||||
if (navigator.userAgent.match(/Windows NT/i)) return false;
|
||||
|
||||
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
|
||||
|
||||
const mq = function (query: string): boolean {
|
||||
return window.matchMedia(query).matches
|
||||
}
|
||||
|
||||
if ('ontouchstart' in window
|
||||
|| (navigator.maxTouchPoints > 0)
|
||||
|| (navigator.msMaxTouchPoints > 0)
|
||||
|| (window as any).DocumentTouch && document instanceof (window as any).DocumentTouch) {
|
||||
return true
|
||||
}
|
||||
|
||||
const query = prefixes.map(i => `(${i}touch-enabled)`).join(',')
|
||||
return mq(query)
|
||||
}
|
7
packages/tl-reference/tsconfig.json
Normal file
7
packages/tl-reference/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": "src"
|
||||
}
|
|
@ -84,7 +84,7 @@ function getJSType(typ, argName) {
|
|||
return normalizeGenerics(typ)
|
||||
}
|
||||
|
||||
async function convertTlToJson(tlText, tlType) {
|
||||
async function convertTlToJson(tlText, tlType, silent = false) {
|
||||
let lines = tlText.split('\n')
|
||||
let pos = 0
|
||||
let line = lines[0].trim()
|
||||
|
@ -130,7 +130,7 @@ async function convertTlToJson(tlText, tlType) {
|
|||
state.type = 'class'
|
||||
return nextLine()
|
||||
}
|
||||
process.stdout.write(
|
||||
if (!silent) process.stdout.write(
|
||||
`[${pad(pos)}/${lines.length}] Processing ${tlType}.tl..\r`
|
||||
)
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ async function convertTlToJson(tlText, tlType) {
|
|||
return ret[name]
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
if (!silent) process.stdout.write(
|
||||
`[${pad(pos)}/${lines.length}] Processing ${tlType}.tl..\r`
|
||||
)
|
||||
|
||||
|
@ -294,7 +294,7 @@ async function convertTlToJson(tlText, tlType) {
|
|||
})
|
||||
})
|
||||
|
||||
console.log(`[${lines.length}/${lines.length}] Processed ${tlType}.tl`)
|
||||
if (!silent) console.log(`[${lines.length}/${lines.length}] Processed ${tlType}.tl`)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
@ -522,4 +522,10 @@ async function main() {
|
|||
await fs.promises.writeFile(path.join(__dirname, '../README.md'), readmeMd)
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
module.exports = {
|
||||
convertTlToJson
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(console.error)
|
||||
}
|
||||
|
|
14
scripts/deploy-docs.bat
Normal file
14
scripts/deploy-docs.bat
Normal file
|
@ -0,0 +1,14 @@
|
|||
@echo off
|
||||
|
||||
cd %~dp0\..\docs
|
||||
echo mt.tei.su > CNAME
|
||||
|
||||
rem reset git repo
|
||||
rd /s /q .git
|
||||
|
||||
git init
|
||||
git add --all . > nul 2> nul
|
||||
git commit -am deploy > nul 2> nul
|
||||
|
||||
git push -f https://github.com/teidesu/mtcute.git master:gh-pages
|
||||
cd ../
|
Loading…
Reference in a new issue