mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Remove old docs, move typedefs dir
This commit is contained in:
parent
4cea07675c
commit
57f730e788
36 changed files with 31 additions and 2456 deletions
|
@ -1,5 +0,0 @@
|
||||||
root: ./docs
|
|
||||||
|
|
||||||
structure:
|
|
||||||
readme: ./README.md
|
|
||||||
summary: ./SUMMARY.md
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
/bin
|
/bin
|
||||||
/target
|
/target
|
||||||
/gitbook
|
|
||||||
|
|
||||||
# Autogenerated files
|
# Autogenerated files
|
||||||
|
|
||||||
|
|
19
.justfile
19
.justfile
|
@ -9,22 +9,3 @@ run-file FILE_NAME:
|
||||||
# Run tests for the Lune library
|
# Run tests for the Lune library
|
||||||
test:
|
test:
|
||||||
cargo test --lib -- --test-threads 1
|
cargo test --lib -- --test-threads 1
|
||||||
|
|
||||||
# Generate gitbook directory
|
|
||||||
generate-gitbook:
|
|
||||||
rm -rf ./gitbook
|
|
||||||
|
|
||||||
mkdir gitbook
|
|
||||||
mkdir gitbook/docs
|
|
||||||
|
|
||||||
cp -R docs gitbook
|
|
||||||
cp README.md gitbook/docs/README.md
|
|
||||||
cp .gitbook.yaml gitbook/.gitbook.yaml
|
|
||||||
|
|
||||||
rm -rf gitbook/docs/typedefs
|
|
||||||
|
|
||||||
cargo run -- --generate-gitbook-dir
|
|
||||||
|
|
||||||
# Publish gitbook directory to gitbook branch
|
|
||||||
publish-gitbook: generate-gitbook
|
|
||||||
npx push-dir --dir=gitbook --branch=gitbook
|
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -4,7 +4,7 @@
|
||||||
"luau-lsp.types.roblox": false,
|
"luau-lsp.types.roblox": false,
|
||||||
"luau-lsp.require.mode": "relativeToFile",
|
"luau-lsp.require.mode": "relativeToFile",
|
||||||
"luau-lsp.require.directoryAliases": {
|
"luau-lsp.require.directoryAliases": {
|
||||||
"@lune/": "./docs/typedefs/"
|
"@lune/": "~/.lune/.typedefs/0.7.4/"
|
||||||
},
|
},
|
||||||
// Luau - ignore type defs file in docs dir and dev scripts we use
|
// Luau - ignore type defs file in docs dir and dev scripts we use
|
||||||
"luau-lsp.ignoreGlobs": [
|
"luau-lsp.ignoreGlobs": [
|
||||||
|
|
|
@ -267,7 +267,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added support for instance tags & `CollectionService` in the `roblox` built-in. <br />
|
- Added support for instance tags & `CollectionService` in the `roblox` built-in. <br />
|
||||||
Currently implemented methods are listed on the [docs site](https://lune.gitbook.io/lune/roblox/api-status).
|
Currently implemented methods are listed on the [docs site](https://lune-org.github.io/docs/roblox/4-api-status).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -305,7 +305,7 @@ This release adds some new features and fixes for the `roblox` built-in.
|
||||||
- Added a `roblox` built-in
|
- Added a `roblox` built-in
|
||||||
|
|
||||||
If you're familiar with [Remodel](https://github.com/rojo-rbx/remodel), this new built-in contains more or less the same APIs, integrated into Lune. <br />
|
If you're familiar with [Remodel](https://github.com/rojo-rbx/remodel), this new built-in contains more or less the same APIs, integrated into Lune. <br />
|
||||||
There are just too many new APIs to list in this changelog, so head over to the [docs sit](https://lune.gitbook.io/lune/roblox/intro) to learn more!
|
There are just too many new APIs to list in this changelog, so head over to the [docs sit](https://lune-org.github.io/docs/roblox/1-introduction) to learn more!
|
||||||
|
|
||||||
- Added a `serde` built-in
|
- Added a `serde` built-in
|
||||||
|
|
||||||
|
|
121
Cargo.lock
generated
121
Cargo.lock
generated
|
@ -198,12 +198,6 @@ version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "beef"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -287,12 +281,6 @@ version = "3.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytecount"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -416,12 +404,6 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
|
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "convert_case"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
|
@ -475,19 +457,6 @@ version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derive_more"
|
|
||||||
version = "0.99.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
|
|
||||||
dependencies = [
|
|
||||||
"convert_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustc_version 0.4.0",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dialoguer"
|
name = "dialoguer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
@ -672,34 +641,6 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "full_moon"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7b9a9bf5e42aec08f4b59be1438d66b01ab0a0f51dca309626e219697b60871c"
|
|
||||||
dependencies = [
|
|
||||||
"bytecount",
|
|
||||||
"cfg-if",
|
|
||||||
"derive_more",
|
|
||||||
"full_moon_derive",
|
|
||||||
"logos",
|
|
||||||
"paste",
|
|
||||||
"serde",
|
|
||||||
"smol_str",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "full_moon_derive"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99b4bd12ce56927d1dc5478d21528ea8c4b93ca85ff8f8043b6a5351a2a3c6f7"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap 1.9.3",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.28"
|
version = "0.3.28"
|
||||||
|
@ -1106,29 +1047,6 @@ version = "0.4.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "logos"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1"
|
|
||||||
dependencies = [
|
|
||||||
"logos-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "logos-derive"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c"
|
|
||||||
dependencies = [
|
|
||||||
"beef",
|
|
||||||
"fnv",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"regex-syntax 0.6.29",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "luau0-src"
|
name = "luau0-src"
|
||||||
version = "0.5.11+luau583"
|
version = "0.5.11+luau583"
|
||||||
|
@ -1151,7 +1069,6 @@ dependencies = [
|
||||||
"directories",
|
"directories",
|
||||||
"dunce",
|
"dunce",
|
||||||
"env_logger 0.10.0",
|
"env_logger 0.10.0",
|
||||||
"full_moon",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glam",
|
"glam",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
@ -1647,7 +1564,7 @@ dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
"regex-syntax 0.7.4",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1658,15 +1575,9 @@ checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.7.4",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.6.29"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
@ -1779,16 +1690,7 @@ version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver 0.9.0",
|
"semver",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc_version"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
|
||||||
dependencies = [
|
|
||||||
"semver 1.0.18",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1896,12 +1798,6 @@ dependencies = [
|
||||||
"semver-parser",
|
"semver-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver-parser"
|
name = "semver-parser"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -2040,15 +1936,6 @@ version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smol_str"
|
|
||||||
version = "0.1.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -2087,7 +1974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
|
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"discard",
|
"discard",
|
||||||
"rustc_version 0.2.3",
|
"rustc_version",
|
||||||
"stdweb-derive",
|
"stdweb-derive",
|
||||||
"stdweb-internal-macros",
|
"stdweb-internal-macros",
|
||||||
"stdweb-internal-runtime",
|
"stdweb-internal-runtime",
|
||||||
|
|
|
@ -24,7 +24,6 @@ cli = [
|
||||||
"dep:env_logger",
|
"dep:env_logger",
|
||||||
"dep:itertools",
|
"dep:itertools",
|
||||||
"dep:clap",
|
"dep:clap",
|
||||||
"dep:full_moon",
|
|
||||||
"dep:include_dir",
|
"dep:include_dir",
|
||||||
"dep:regex",
|
"dep:regex",
|
||||||
]
|
]
|
||||||
|
@ -111,7 +110,6 @@ env_logger = { optional = true, version = "0.10" }
|
||||||
itertools = { optional = true, version = "0.10" }
|
itertools = { optional = true, version = "0.10" }
|
||||||
|
|
||||||
clap = { optional = true, version = "4.1", features = ["derive"] }
|
clap = { optional = true, version = "4.1", features = ["derive"] }
|
||||||
full_moon = { optional = true, version = "0.18", features = ["roblox"] }
|
|
||||||
include_dir = { optional = true, version = "0.7.3", features = ["glob"] }
|
include_dir = { optional = true, version = "0.7.3", features = ["glob"] }
|
||||||
regex = { optional = true, version = "1.7", default-features = false, features = [
|
regex = { optional = true, version = "1.7", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Summary
|
|
||||||
|
|
||||||
## Home
|
|
||||||
|
|
||||||
- [Installation](pages/home/Installation.md)
|
|
||||||
- [Writing Scripts](pages/home/Writing-Scripts.md)
|
|
||||||
- [Running Scripts](pages/home/Running-Scripts.md)
|
|
||||||
- [Editor Setup](pages/home/Editor-Setup.md)
|
|
||||||
|
|
||||||
## Roblox
|
|
||||||
|
|
||||||
- [Introduction](pages/roblox/Introduction.md)
|
|
||||||
- [Examples](pages/roblox/Examples.md)
|
|
||||||
- [API Status](pages/roblox/Api-Status.md)
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
- [fs](pages/api/fs.md)
|
|
||||||
- [net](pages/api/net.md)
|
|
||||||
- [process](pages/api/process.md)
|
|
||||||
- [roblox](pages/api/roblox.md)
|
|
||||||
- [serde](pages/api/serde.md)
|
|
||||||
- [stdio](pages/api/stdio.md)
|
|
||||||
- [task](pages/api/task.md)
|
|
|
@ -1,283 +0,0 @@
|
||||||
--!strict
|
|
||||||
|
|
||||||
local fs = require("@lune/fs")
|
|
||||||
local net = require("@lune/net")
|
|
||||||
local serde = require("@lune/serde")
|
|
||||||
local process = require("@lune/process")
|
|
||||||
local roblox = require("@lune/roblox")
|
|
||||||
|
|
||||||
export type LuneDataModel = roblox.DataModel
|
|
||||||
export type LuneInstance = roblox.Instance
|
|
||||||
|
|
||||||
local function getAuthCookieWithFallbacks()
|
|
||||||
local cookie = roblox.getAuthCookie()
|
|
||||||
if cookie then
|
|
||||||
return cookie
|
|
||||||
end
|
|
||||||
|
|
||||||
local cookieFromEnv = process.env.REMODEL_AUTH
|
|
||||||
if cookieFromEnv and #cookieFromEnv > 0 then
|
|
||||||
return `.ROBLOSECURITY={cookieFromEnv}`
|
|
||||||
end
|
|
||||||
|
|
||||||
for index, arg in process.args do
|
|
||||||
if arg == "--auth" then
|
|
||||||
local cookieFromArgs = process.args[index + 1]
|
|
||||||
if cookieFromArgs and #cookieFromArgs > 0 then
|
|
||||||
return `.ROBLOSECURITY={cookieFromArgs}`
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
error([[
|
|
||||||
Failed to find ROBLOSECURITY cookie for authentication!
|
|
||||||
Make sure you have logged into studio, or set the ROBLOSECURITY environment variable.
|
|
||||||
]])
|
|
||||||
end
|
|
||||||
|
|
||||||
local function downloadAssetId(assetId: number)
|
|
||||||
-- 1. Try to find the auth cookie for the current user
|
|
||||||
local cookie = getAuthCookieWithFallbacks()
|
|
||||||
|
|
||||||
-- 2. Send a request to the asset delivery API,
|
|
||||||
-- which will respond with cdn download link(s)
|
|
||||||
local assetApiResponse = net.request({
|
|
||||||
url = `https://assetdelivery.roblox.com/v2/assetId/{assetId}`,
|
|
||||||
headers = {
|
|
||||||
Accept = "application/json",
|
|
||||||
Cookie = cookie,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if not assetApiResponse.ok then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Failed to fetch asset download link for asset id %s!\n%s (%s)\n%s",
|
|
||||||
tostring(assetId),
|
|
||||||
tostring(assetApiResponse.statusCode),
|
|
||||||
tostring(assetApiResponse.statusMessage),
|
|
||||||
tostring(assetApiResponse.body)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 3. Make sure we got a valid response body
|
|
||||||
local assetApiBody = serde.decode("json", assetApiResponse.body)
|
|
||||||
if type(assetApiBody) ~= "table" then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Asset delivery API returned an invalid response body!\n%s",
|
|
||||||
assetApiResponse.body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elseif type(assetApiBody.locations) ~= "table" then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Asset delivery API returned an invalid response body!\n%s",
|
|
||||||
assetApiResponse.body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 4. Grab the first asset download location - we only
|
|
||||||
-- requested one in our query, so this will be correct
|
|
||||||
local firstLocation = assetApiBody.locations[1]
|
|
||||||
if type(firstLocation) ~= "table" then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Asset delivery API returned no download locations!\n%s",
|
|
||||||
assetApiResponse.body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elseif type(firstLocation.location) ~= "string" then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Asset delivery API returned no valid download locations!\n%s",
|
|
||||||
assetApiResponse.body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 5. Fetch the place contents from the cdn
|
|
||||||
local cdnResponse = net.request({
|
|
||||||
url = firstLocation.location,
|
|
||||||
headers = {
|
|
||||||
Cookie = cookie,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if not cdnResponse.ok then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Failed to download asset with id %s from the Roblox cdn!\n%s (%s)\n%s",
|
|
||||||
tostring(assetId),
|
|
||||||
tostring(cdnResponse.statusCode),
|
|
||||||
tostring(cdnResponse.statusMessage),
|
|
||||||
tostring(cdnResponse.body)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 6. The response body should now be the contents of the asset file
|
|
||||||
return cdnResponse.body
|
|
||||||
end
|
|
||||||
|
|
||||||
local function uploadAssetId(assetId: number, contents: string)
|
|
||||||
-- 1. Try to find the auth cookie for the current user
|
|
||||||
local cookie = getAuthCookieWithFallbacks()
|
|
||||||
|
|
||||||
-- 2. Create request headers in advance, we might re-use them for CSRF challenges
|
|
||||||
local headers = {
|
|
||||||
["User-Agent"] = "Roblox/WinInet",
|
|
||||||
["Content-Type"] = "application/octet-stream",
|
|
||||||
Accept = "application/json",
|
|
||||||
Cookie = cookie,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- 3. Create and send a request to the upload url
|
|
||||||
local uploadResponse = net.request({
|
|
||||||
url = `https://data.roblox.com/Data/Upload.ashx?assetid={assetId}`,
|
|
||||||
body = contents,
|
|
||||||
method = "POST",
|
|
||||||
headers = headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- 4. Check if we got a valid response, we might have gotten a CSRF
|
|
||||||
-- challenge and need to send the request with a token included
|
|
||||||
if
|
|
||||||
not uploadResponse.ok
|
|
||||||
and uploadResponse.statusCode == 403
|
|
||||||
and uploadResponse.headers["x-csrf-token"] ~= nil
|
|
||||||
then
|
|
||||||
headers["X-CSRF-Token"] = uploadResponse.headers["x-csrf-token"]
|
|
||||||
uploadResponse = net.request({
|
|
||||||
url = `https://data.roblox.com/Data/Upload.ashx?assetid={assetId}`,
|
|
||||||
body = contents,
|
|
||||||
method = "POST",
|
|
||||||
headers = headers,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if not uploadResponse.ok then
|
|
||||||
error(
|
|
||||||
string.format(
|
|
||||||
"Failed to upload asset with id %s to Roblox!\n%s (%s)\n%s",
|
|
||||||
tostring(assetId),
|
|
||||||
tostring(uploadResponse.statusCode),
|
|
||||||
tostring(uploadResponse.statusMessage),
|
|
||||||
tostring(uploadResponse.body)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local remodel = {}
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Load an `rbxl` or `rbxlx` file from the filesystem.
|
|
||||||
|
|
||||||
Returns a `DataModel` instance, equivalent to `game` from within Roblox.
|
|
||||||
]=]
|
|
||||||
function remodel.readPlaceFile(filePath: string)
|
|
||||||
local placeFile = fs.readFile(filePath)
|
|
||||||
local place = roblox.deserializePlace(placeFile)
|
|
||||||
return place
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Load an `rbxm` or `rbxmx` file from the filesystem.
|
|
||||||
|
|
||||||
Note that this function returns a **list of instances** instead of a single instance!
|
|
||||||
This is because models can contain mutliple top-level instances.
|
|
||||||
]=]
|
|
||||||
function remodel.readModelFile(filePath: string)
|
|
||||||
local modelFile = fs.readFile(filePath)
|
|
||||||
local model = roblox.deserializeModel(modelFile)
|
|
||||||
return model
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Reads a place asset from Roblox, equivalent to `remodel.readPlaceFile`.
|
|
||||||
|
|
||||||
***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
|
|
||||||
]=]
|
|
||||||
function remodel.readPlaceAsset(assetId: number)
|
|
||||||
local contents = downloadAssetId(assetId)
|
|
||||||
local place = roblox.deserializePlace(contents)
|
|
||||||
return place
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Reads a model asset from Roblox, equivalent to `remodel.readModelFile`.
|
|
||||||
|
|
||||||
***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
|
|
||||||
]=]
|
|
||||||
function remodel.readModelAsset(assetId: number)
|
|
||||||
local contents = downloadAssetId(assetId)
|
|
||||||
local place = roblox.deserializeModel(contents)
|
|
||||||
return place
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Saves an `rbxl` or `rbxlx` file out of the given `DataModel` instance.
|
|
||||||
|
|
||||||
If the instance is not a `DataModel`, this function will throw.
|
|
||||||
Models should be saved with `writeModelFile` instead.
|
|
||||||
]=]
|
|
||||||
function remodel.writePlaceFile(filePath: string, dataModel: LuneDataModel)
|
|
||||||
local asBinary = string.sub(filePath, -5) == ".rbxl"
|
|
||||||
local asXml = string.sub(filePath, -6) == ".rbxlx"
|
|
||||||
assert(asBinary or asXml, "File path must have .rbxl or .rbxlx extension")
|
|
||||||
local placeFile = roblox.serializePlace(dataModel, asXml)
|
|
||||||
fs.writeFile(filePath, placeFile)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Saves an `rbxm` or `rbxmx` file out of the given `Instance`.
|
|
||||||
|
|
||||||
If the instance is a `DataModel`, this function will throw.
|
|
||||||
Places should be saved with `writePlaceFile` instead.
|
|
||||||
]=]
|
|
||||||
function remodel.writeModelFile(filePath: string, instance: LuneInstance)
|
|
||||||
local asBinary = string.sub(filePath, -5) == ".rbxm"
|
|
||||||
local asXml = string.sub(filePath, -6) == ".rbxmx"
|
|
||||||
assert(asBinary or asXml, "File path must have .rbxm or .rbxmx extension")
|
|
||||||
local placeFile = roblox.serializeModel({ instance }, asXml)
|
|
||||||
fs.writeFile(filePath, placeFile)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Uploads the given `DataModel` instance to Roblox, overwriting an existing place.
|
|
||||||
|
|
||||||
If the instance is not a `DataModel`, this function will throw.
|
|
||||||
Models should be uploaded with `writeExistingModelAsset` instead.
|
|
||||||
|
|
||||||
***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
|
|
||||||
]=]
|
|
||||||
function remodel.writeExistingPlaceAsset(dataModel: LuneDataModel, assetId: number)
|
|
||||||
local placeFile = roblox.serializePlace(dataModel)
|
|
||||||
uploadAssetId(assetId, placeFile)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[=[
|
|
||||||
Uploads the given instance to Roblox, overwriting an existing model.
|
|
||||||
|
|
||||||
If the instance is a `DataModel`, this function will throw.
|
|
||||||
Places should be uploaded with `writeExistingPlaceAsset` instead.
|
|
||||||
|
|
||||||
***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
|
|
||||||
]=]
|
|
||||||
function remodel.writeExistingModelAsset(instance: LuneInstance, assetId: number)
|
|
||||||
local modelFile = roblox.serializeModel({ instance })
|
|
||||||
uploadAssetId(assetId, modelFile)
|
|
||||||
end
|
|
||||||
|
|
||||||
remodel.readFile = fs.readFile
|
|
||||||
remodel.readDir = fs.readDir
|
|
||||||
remodel.writeFile = fs.writeFile
|
|
||||||
remodel.createDirAll = fs.writeDir
|
|
||||||
remodel.removeFile = fs.removeFile
|
|
||||||
remodel.removeDir = fs.removeDir
|
|
||||||
remodel.isFile = fs.isFile
|
|
||||||
remodel.isDir = fs.isDir
|
|
||||||
|
|
||||||
return remodel
|
|
|
@ -1,22 +0,0 @@
|
||||||
# 🧑💻 Configuring VSCode and tooling for Lune
|
|
||||||
|
|
||||||
Lune puts developer experience first, and as such provides type definitions and configurations for several tools out of the box.
|
|
||||||
|
|
||||||
These steps assume you have already installed Lune and that it is available to run in the current directory.
|
|
||||||
|
|
||||||
## Luau LSP
|
|
||||||
|
|
||||||
1. Run `lune --setup` to generate Luau type definitions for your installed version of Lune
|
|
||||||
2. Verify that type definition files have been generated
|
|
||||||
3. Modify your VSCode settings, either by using the settings menu or in `settings.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"luau-lsp.require.mode": "relativeToFile", // Set the require mode to work with Lune
|
|
||||||
"luau-lsp.require.fileAliases": { // Add type definitions for Lune builtins
|
|
||||||
"@lune/fs": ".../.lune/.typedefs/x.y.z/fs.luau",
|
|
||||||
"@lune/net": ".../.lune/.typedefs/x.y.z/net.luau",
|
|
||||||
"@lune/...": "..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
_**NOTE:** If you already had a `.vscode/settings.json` file in your current directory the type definition files may have been added automatically!_
|
|
|
@ -1,36 +0,0 @@
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
|
|
||||||
# ⚙️ Installation
|
|
||||||
|
|
||||||
The preferred way of installing Lune is using [Aftman](https://github.com/lpghatguy/aftman).
|
|
||||||
|
|
||||||
Running this command in your terminal will add `lune` to an `aftman.toml` file in the current directory, or create one if it does not exist:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
aftman add filiptibell/lune
|
|
||||||
```
|
|
||||||
|
|
||||||
## Other options
|
|
||||||
|
|
||||||
### Building from source
|
|
||||||
|
|
||||||
Building and installing from source requires the latest version of [Rust & Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) to be installed on your system. <br />
|
|
||||||
Once installed, run the following command in your terminal:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo install lune --locked
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that Lune does not make any minimum supported rust version (MSRV) guarantees and you may need to upgrade your version of Rust to update Lune in the future.
|
|
||||||
|
|
||||||
### Using GitHub Releases
|
|
||||||
|
|
||||||
You can download pre-built binaries for most systems directly from the [GitHub Releases](https://github.com/filiptibell/lune/releases) page. <br />
|
|
||||||
There are many tools that can install binaries directly from releases, and it is up to you to choose what tool to use when installing here.
|
|
||||||
|
|
||||||
## Next steps
|
|
||||||
|
|
||||||
Congratulations! You've installed Lune and are now ready to write your first script.
|
|
||||||
|
|
||||||
- If you want to write standalone scripts, head over to the [Writing Scripts](https://lune.gitbook.io/lune/home/writing-scripts) page.
|
|
||||||
- If you want to write Lune scripts specifically for Roblox, check out the [Roblox](https://lune.gitbook.io/lune/roblox/intro) section.
|
|
|
@ -1,51 +0,0 @@
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
|
|
||||||
# 🏃 Running Lune Scripts
|
|
||||||
|
|
||||||
After you've written a script file, for example `script-name.luau`, you can run it:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
lune script-name
|
|
||||||
```
|
|
||||||
|
|
||||||
This will look for the file `script-name.luau`**_<sup>[1]</sup>_** in a few locations:
|
|
||||||
|
|
||||||
- The current directory
|
|
||||||
- The folder `lune` in the current directory, if it exists
|
|
||||||
- The folder `.lune` in the current directory, if it exists
|
|
||||||
- The folder `lune` in the _home_ directory, if it exists
|
|
||||||
- The folder `.lune` in the _home_ directory, if it exists
|
|
||||||
|
|
||||||
## 🎛️ Passing Command-Line Arguments
|
|
||||||
|
|
||||||
Arguments can be passed to a Lune script directory from the command line when running it:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
lune script-name arg1 arg2 "argument three"
|
|
||||||
```
|
|
||||||
|
|
||||||
These arguments will then be available in your script using `process.args`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
print(process.args)
|
|
||||||
--> { "arg1", "arg2", "argument three" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💭 Additional Commands
|
|
||||||
|
|
||||||
```sh
|
|
||||||
lune --list
|
|
||||||
```
|
|
||||||
|
|
||||||
Lists all scripts found in `lune` or `.lune` directories, including any top-level description comments. <br />
|
|
||||||
Lune description comments are always written at the top of a file and start with a lua-style comment arrow (`-->`).
|
|
||||||
|
|
||||||
```sh
|
|
||||||
lune -
|
|
||||||
```
|
|
||||||
|
|
||||||
Runs a script passed to Lune using stdin. Occasionally useful for running scripts piped to Lune from external sources.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**_<sup>[1]</sup>_** _Lune also supports files with the `.lua` extension but using the `.luau` extension is highly recommended. Additionally, if you don't want Lune to look in sub-directories or try to find files with `.lua` / `.luau` extensions at all, you can provide an absolute file path. This will disable all file path parsing and checks, and just run the file directly._
|
|
|
@ -1,326 +0,0 @@
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
<!-- markdownlint-disable MD026 -->
|
|
||||||
|
|
||||||
# ✏️ Writing Lune Scripts
|
|
||||||
|
|
||||||
If you've already written some version of Lua (or Luau) scripts before, this walkthrough will make you feel right at home.
|
|
||||||
|
|
||||||
Once you have a script you want to run, head over to the [Running Scripts](https://lune.gitbook.io/lune/home/running-scripts) page.
|
|
||||||
|
|
||||||
## Hello, Lune!
|
|
||||||
|
|
||||||
```lua
|
|
||||||
--[[
|
|
||||||
EXAMPLE #1
|
|
||||||
|
|
||||||
Using arguments given to the program
|
|
||||||
]]
|
|
||||||
|
|
||||||
if #process.args > 0 then
|
|
||||||
print("Got arguments:")
|
|
||||||
print(process.args)
|
|
||||||
if #process.args > 3 then
|
|
||||||
error("Too many arguments!")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
print("Got no arguments ☹️")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #2
|
|
||||||
|
|
||||||
Using the stdio library to prompt for terminal input
|
|
||||||
]]
|
|
||||||
|
|
||||||
local text = stdio.prompt("text", "Please write some text")
|
|
||||||
|
|
||||||
print("You wrote '" .. text .. "'!")
|
|
||||||
|
|
||||||
local confirmed = stdio.prompt("confirm", "Please confirm that you wrote some text")
|
|
||||||
if confirmed == false then
|
|
||||||
error("You didn't confirm!")
|
|
||||||
else
|
|
||||||
print("Confirmed!")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #3
|
|
||||||
|
|
||||||
Get & set environment variables
|
|
||||||
|
|
||||||
Checks if environment variables are empty or not,
|
|
||||||
prints out ❌ if empty and ✅ if they have a value
|
|
||||||
]]
|
|
||||||
|
|
||||||
print("Reading current environment 🔎")
|
|
||||||
|
|
||||||
-- Environment variables can be read directly
|
|
||||||
assert(process.env.PATH ~= nil, "Missing PATH")
|
|
||||||
assert(process.env.PWD ~= nil, "Missing PWD")
|
|
||||||
|
|
||||||
-- And they can also be accessed using Luau's generalized iteration (but not pairs())
|
|
||||||
for key, value in process.env do
|
|
||||||
local box = if value and value ~= "" then "✅" else "❌"
|
|
||||||
print(string.format("[%s] %s", box, key))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #4
|
|
||||||
|
|
||||||
Writing a module
|
|
||||||
|
|
||||||
Modularizing and splitting up your code is Lune is very straight-forward,
|
|
||||||
in contrast to other scripting languages and shells such as bash
|
|
||||||
]]
|
|
||||||
|
|
||||||
local module = {}
|
|
||||||
|
|
||||||
function module.sayHello()
|
|
||||||
print("Hello, Lune! 🌙")
|
|
||||||
end
|
|
||||||
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #5
|
|
||||||
|
|
||||||
Using a function from another module / script
|
|
||||||
|
|
||||||
Lune has path-relative imports, similar to other popular languages such as JavaScript
|
|
||||||
]]
|
|
||||||
|
|
||||||
local module = require("../modules/module")
|
|
||||||
module.sayHello()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #6
|
|
||||||
|
|
||||||
Spawning concurrent tasks
|
|
||||||
|
|
||||||
These tasks will run at the same time as other Lua code which lets you do primitive multitasking
|
|
||||||
]]
|
|
||||||
|
|
||||||
task.spawn(function()
|
|
||||||
print("Spawned a task that will run instantly but not block")
|
|
||||||
task.wait(5)
|
|
||||||
end)
|
|
||||||
|
|
||||||
print("Spawning a delayed task that will run in 5 seconds")
|
|
||||||
task.delay(5, function()
|
|
||||||
print("...")
|
|
||||||
task.wait(1)
|
|
||||||
print("Hello again!")
|
|
||||||
task.wait(1)
|
|
||||||
print("Goodbye again! 🌙")
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #7
|
|
||||||
|
|
||||||
Read files in the current directory
|
|
||||||
|
|
||||||
This prints out directory & file names with some fancy icons
|
|
||||||
]]
|
|
||||||
|
|
||||||
print("Reading current dir 🗂️")
|
|
||||||
local entries = fs.readDir(".")
|
|
||||||
|
|
||||||
-- NOTE: We have to do this outside of the sort function
|
|
||||||
-- to avoid yielding across the metamethod boundary, all
|
|
||||||
-- of the filesystem APIs are asynchronous and yielding
|
|
||||||
local entryIsDir = {}
|
|
||||||
for _, entry in entries do
|
|
||||||
entryIsDir[entry] = fs.isDir(entry)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sort prioritizing directories first, then alphabetically
|
|
||||||
table.sort(entries, function(entry0, entry1)
|
|
||||||
if entryIsDir[entry0] ~= entryIsDir[entry1] then
|
|
||||||
return entryIsDir[entry0]
|
|
||||||
end
|
|
||||||
return entry0 < entry1
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Make sure we got some known files that should always exist
|
|
||||||
assert(table.find(entries, "Cargo.toml") ~= nil, "Missing Cargo.toml")
|
|
||||||
assert(table.find(entries, "Cargo.lock") ~= nil, "Missing Cargo.lock")
|
|
||||||
|
|
||||||
-- Print the pretty stuff
|
|
||||||
for _, entry in entries do
|
|
||||||
if fs.isDir(entry) then
|
|
||||||
print("📁 " .. entry)
|
|
||||||
else
|
|
||||||
print("📄 " .. entry)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #8
|
|
||||||
|
|
||||||
Call out to another program / executable
|
|
||||||
|
|
||||||
You can also get creative and combine this with example #6 to spawn several programs at the same time!
|
|
||||||
]]
|
|
||||||
|
|
||||||
print("Sending 4 pings to google 🌏")
|
|
||||||
local result = process.spawn("ping", {
|
|
||||||
"google.com",
|
|
||||||
"-c 4",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #9
|
|
||||||
|
|
||||||
Using the result of a spawned process, exiting the process
|
|
||||||
|
|
||||||
This looks scary with lots of weird symbols, but, it's just some Lua-style pattern matching
|
|
||||||
to parse the lines of "min/avg/max/stddev = W/X/Y/Z ms" that the ping program outputs to us
|
|
||||||
]]
|
|
||||||
|
|
||||||
if result.ok then
|
|
||||||
assert(#result.stdout > 0, "Result output was empty")
|
|
||||||
local min, avg, max, stddev = string.match(
|
|
||||||
result.stdout,
|
|
||||||
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
|
|
||||||
)
|
|
||||||
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
|
|
||||||
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
|
|
||||||
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
|
|
||||||
print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev))))
|
|
||||||
else
|
|
||||||
print("Failed to send ping to google!")
|
|
||||||
print(result.stderr)
|
|
||||||
process.exit(result.code)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #10
|
|
||||||
|
|
||||||
Using the built-in networking library, encoding & decoding json
|
|
||||||
]]
|
|
||||||
|
|
||||||
print("Sending PATCH request to web API 📤")
|
|
||||||
local apiResult = net.request({
|
|
||||||
url = "https://jsonplaceholder.typicode.com/posts/1",
|
|
||||||
method = "PATCH",
|
|
||||||
headers = {
|
|
||||||
["Content-Type"] = "application/json",
|
|
||||||
},
|
|
||||||
body = net.jsonEncode({
|
|
||||||
title = "foo",
|
|
||||||
body = "bar",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if not apiResult.ok then
|
|
||||||
print("Failed to send network request!")
|
|
||||||
print(string.format("%d (%s)", apiResult.statusCode, apiResult.statusMessage))
|
|
||||||
print(apiResult.body)
|
|
||||||
process.exit(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
type ApiResponse = {
|
|
||||||
id: number,
|
|
||||||
title: string,
|
|
||||||
body: string,
|
|
||||||
userId: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
local apiResponse: ApiResponse = net.jsonDecode(apiResult.body)
|
|
||||||
assert(apiResponse.title == "foo", "Invalid json response")
|
|
||||||
assert(apiResponse.body == "bar", "Invalid json response")
|
|
||||||
print("Got valid JSON response with changes applied")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #11
|
|
||||||
|
|
||||||
Using the stdio library to print pretty
|
|
||||||
]]
|
|
||||||
|
|
||||||
print("Printing with pretty colors and auto-formatting 🎨")
|
|
||||||
|
|
||||||
print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset"))
|
|
||||||
|
|
||||||
print("API response:", apiResponse)
|
|
||||||
warn({
|
|
||||||
Oh = {
|
|
||||||
No = {
|
|
||||||
TooMuch = {
|
|
||||||
Nesting = {
|
|
||||||
"Will not print",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset"))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[
|
|
||||||
EXAMPLE #12
|
|
||||||
|
|
||||||
Saying goodbye 😔
|
|
||||||
]]
|
|
||||||
|
|
||||||
print("Goodbye, lune! 🌙")
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
More real-world examples of how to write Lune scripts can be found in the [examples](https://github.com/filiptibell/lune/blob/main/.lune/examples/) folder.
|
|
||||||
|
|
||||||
Documentation for individual APIs and types can be found in "API Reference" in the sidebar of this wiki.
|
|
||||||
|
|
||||||
## Extras
|
|
||||||
|
|
||||||
### 🔀 Example translation from Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
VALID=true
|
|
||||||
COUNT=1
|
|
||||||
while [ $VALID ]
|
|
||||||
do
|
|
||||||
echo $COUNT
|
|
||||||
if [ $COUNT -eq 5 ];
|
|
||||||
then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
((COUNT++))
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
**_With Lune & Luau:_**
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local valid = true
|
|
||||||
local count = 1
|
|
||||||
while valid do
|
|
||||||
print(count)
|
|
||||||
if count == 5 then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
count += 1
|
|
||||||
end
|
|
||||||
```
|
|
|
@ -1,76 +0,0 @@
|
||||||
<!-- markdownlint-disable MD041 -->
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
|
|
||||||
# API Status
|
|
||||||
|
|
||||||
This is a page indicating the current implementation status for instance methods and datatypes in the `roblox` library.
|
|
||||||
|
|
||||||
If an API on a class is not listed here it may not be within the scope for Lune and may not be implemented in the future. <br />
|
|
||||||
However, if a recently added datatype is missing, and it can be used as an instance property, it is likely that it will be implemented.
|
|
||||||
|
|
||||||
## Classes
|
|
||||||
|
|
||||||
### `Instance`
|
|
||||||
|
|
||||||
Currently implemented APIs:
|
|
||||||
|
|
||||||
- [`new`](https://create.roblox.com/docs/reference/engine/datatypes/Instance#new) - note that this does not include the second `parent` argument
|
|
||||||
- [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
|
|
||||||
- [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
|
|
||||||
- [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
|
|
||||||
- [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
|
|
||||||
- [`FindFirstAncestor`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestor)
|
|
||||||
- [`FindFirstAncestorOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorOfClass)
|
|
||||||
- [`FindFirstAncestorWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorWhichIsA)
|
|
||||||
- [`FindFirstChild`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChild)
|
|
||||||
- [`FindFirstChildOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildOfClass)
|
|
||||||
- [`FindFirstChildWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildWhichIsA)
|
|
||||||
- [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
|
|
||||||
- [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
|
|
||||||
- [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
|
|
||||||
- [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
|
|
||||||
- [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
|
|
||||||
- [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
|
|
||||||
- [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
|
|
||||||
- [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
|
|
||||||
- [`IsAncestorOf`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsAncestorOf)
|
|
||||||
- [`IsDescendantOf`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsDescendantOf)
|
|
||||||
- [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
|
|
||||||
- [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
|
|
||||||
|
|
||||||
### `DataModel`
|
|
||||||
|
|
||||||
Currently implemented APIs:
|
|
||||||
|
|
||||||
- [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
|
|
||||||
- [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
|
|
||||||
|
|
||||||
## Datatypes
|
|
||||||
|
|
||||||
Currently implemented datatypes:
|
|
||||||
|
|
||||||
- [`Axes`](https://create.roblox.com/docs/reference/engine/datatypes/Axes)
|
|
||||||
- [`BrickColor`](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor)
|
|
||||||
- [`CFrame`](https://create.roblox.com/docs/reference/engine/datatypes/CFrame)
|
|
||||||
- [`Color3`](https://create.roblox.com/docs/reference/engine/datatypes/Color3)
|
|
||||||
- [`ColorSequence`](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence)
|
|
||||||
- [`ColorSequenceKeypoint`](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint)
|
|
||||||
- [`Enum`](https://create.roblox.com/docs/reference/engine/datatypes/Enum)
|
|
||||||
- [`Faces`](https://create.roblox.com/docs/reference/engine/datatypes/Faces)
|
|
||||||
- [`Font`](https://create.roblox.com/docs/reference/engine/datatypes/Font)
|
|
||||||
- [`NumberRange`](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange)
|
|
||||||
- [`NumberSequence`](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence)
|
|
||||||
- [`NumberSequenceKeypoint`](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint)
|
|
||||||
- [`PhysicalProperties`](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties)
|
|
||||||
- [`Ray`](https://create.roblox.com/docs/reference/engine/datatypes/Ray)
|
|
||||||
- [`Rect`](https://create.roblox.com/docs/reference/engine/datatypes/Rect)
|
|
||||||
- [`Region3`](https://create.roblox.com/docs/reference/engine/datatypes/Region3)
|
|
||||||
- [`Region3int16`](https://create.roblox.com/docs/reference/engine/datatypes/Region3int16)
|
|
||||||
- [`UDim`](https://create.roblox.com/docs/reference/engine/datatypes/UDim)
|
|
||||||
- [`UDim2`](https://create.roblox.com/docs/reference/engine/datatypes/UDim2)
|
|
||||||
- [`Vector2`](https://create.roblox.com/docs/reference/engine/datatypes/Vector2)
|
|
||||||
- [`Vector2int16`](https://create.roblox.com/docs/reference/engine/datatypes/Vector2int16)
|
|
||||||
- [`Vector3`](https://create.roblox.com/docs/reference/engine/datatypes/Vector3)
|
|
||||||
- [`Vector3int16`](https://create.roblox.com/docs/reference/engine/datatypes/Vector3int16)
|
|
||||||
|
|
||||||
Note that these datatypes are kept as up-to-date as possible, but recently added members & methods may be missing.
|
|
|
@ -1,78 +0,0 @@
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
<!-- markdownlint-disable MD026 -->
|
|
||||||
|
|
||||||
# Example Lune Scripts for Roblox
|
|
||||||
|
|
||||||
These are a few examples of things you can do using the built-in `roblox` library.
|
|
||||||
|
|
||||||
## `1` - Make all parts anchored in a place file
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local roblox = require("@lune/roblox")
|
|
||||||
|
|
||||||
-- Read the place file called myPlaceFile.rbxl into a DataModel called "game"
|
|
||||||
-- This works exactly the same as in Roblox, except "game" does not exist by default - you have to load it from a file!
|
|
||||||
local game = roblox.readPlaceFile("myPlaceFile.rbxl")
|
|
||||||
local workspace = game:GetService("Workspace")
|
|
||||||
|
|
||||||
-- Make all of the parts in the workspace anchored
|
|
||||||
for _, descendant in workspace:GetDescendants() do
|
|
||||||
if descendant:IsA("BasePart") then
|
|
||||||
descendant.Anchored = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Save the DataModel (game) back to the file that we read it from
|
|
||||||
roblox.writePlaceFile("myPlaceFile.rbxl")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `2` - Save instances in a place as individual model files
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local roblox = require("@lune/roblox")
|
|
||||||
local fs = require("@lune/fs")
|
|
||||||
|
|
||||||
-- Here we load a file just like in the first example
|
|
||||||
local game = roblox.readPlaceFile("myPlaceFile.rbxl")
|
|
||||||
local workspace = game:GetService("Workspace")
|
|
||||||
|
|
||||||
-- We use a normal Lune API to make sure a directory exists to save our models in
|
|
||||||
fs.writeDir("models")
|
|
||||||
|
|
||||||
-- Then we save all of our instances in Workspace as model files, in our new directory
|
|
||||||
-- Note that a model file can actually contain several instances at once, so we pass a table here
|
|
||||||
for _, child in workspace:GetChildren() do
|
|
||||||
roblox.writeModelFile("models/" .. child.Name, { child })
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `3` - Make a new place from scratch
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local roblox = require("@lune/roblox")
|
|
||||||
local Instance = roblox.Instance
|
|
||||||
|
|
||||||
-- You can even create a new DataModel using Instance.new, which is not normally possible in Roblox
|
|
||||||
-- This is normal - most instances that are not normally accessible in Roblox can be manipulated using Lune!
|
|
||||||
local game = Instance.new("DataModel")
|
|
||||||
local workspace = game:GetService("Workspace")
|
|
||||||
|
|
||||||
-- Here we just make a bunch of models with parts in them for demonstration purposes
|
|
||||||
for i = 1, 50 do
|
|
||||||
local model = Instance.new("Model")
|
|
||||||
model.Name = "Model #" .. tostring(i)
|
|
||||||
model.Parent = workspace
|
|
||||||
for j = 1, 4 do
|
|
||||||
local part = Instance.new("Part")
|
|
||||||
part.Name = "Part #" .. tostring(j)
|
|
||||||
part.Parent = model
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- As always, we have to save the DataModel (game) to a file when we're done
|
|
||||||
roblox.writePlaceFile("myPlaceWithLotsOfModels.rbxl")
|
|
||||||
```
|
|
|
@ -1,10 +0,0 @@
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
<!-- markdownlint-disable MD026 -->
|
|
||||||
|
|
||||||
# ✏️ Writing Lune Scripts for Roblox
|
|
||||||
|
|
||||||
Lune has a powerful built-in library and set of APIs for manipulating Roblox place files and model files. It contains APIs for reading & writing files, and gives you instances to use, just as if you were scripting inside of the Roblox engine, albeit with a more limited API.
|
|
||||||
|
|
||||||
For examples on how to write Roblox-specific Lune scripts, check out the [Examples](https://lune.gitbook.io/lune/roblox/examples) page.
|
|
||||||
|
|
||||||
For a full list of the currently implemented Roblox APIs, check out the [API Status](https://lune.gitbook.io/lune/roblox/api-status) page.
|
|
|
@ -2,14 +2,32 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use directories::UserDirs;
|
use directories::UserDirs;
|
||||||
|
|
||||||
use futures_util::future::try_join_all;
|
use futures_util::future::try_join_all;
|
||||||
|
use include_dir::Dir;
|
||||||
use tokio::fs::{create_dir_all, write};
|
use tokio::fs::{create_dir_all, write};
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
|
||||||
pub async fn generate_from_type_definitions(
|
let contents = read_typedefs_dir_contents(dir);
|
||||||
typedef_files: HashMap<String, Vec<u8>>,
|
write_typedef_files(contents).await
|
||||||
) -> Result<String> {
|
}
|
||||||
|
|
||||||
|
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
|
||||||
|
let mut definitions = HashMap::new();
|
||||||
|
|
||||||
|
for entry in dir.find("*.luau").unwrap() {
|
||||||
|
let entry_file = entry.as_file().unwrap();
|
||||||
|
let entry_name = entry_file.path().file_name().unwrap().to_string_lossy();
|
||||||
|
|
||||||
|
let typedef_name = entry_name.trim_end_matches(".luau");
|
||||||
|
let typedef_contents = entry_file.contents().to_vec();
|
||||||
|
|
||||||
|
definitions.insert(typedef_name.to_string(), typedef_contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
definitions
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_typedef_files(typedef_files: HashMap<String, Vec<u8>>) -> Result<String> {
|
||||||
let version_string = env!("CARGO_PKG_VERSION");
|
let version_string = env!("CARGO_PKG_VERSION");
|
||||||
let mut dirs_to_write = Vec::new();
|
let mut dirs_to_write = Vec::new();
|
||||||
let mut files_to_write = Vec::new();
|
let mut files_to_write = Vec::new();
|
|
@ -1,129 +0,0 @@
|
||||||
use anyhow::{bail, Result};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
item::{DefinitionsItem, DefinitionsItemFunctionArg, DefinitionsItemFunctionRet},
|
|
||||||
kind::DefinitionsItemKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct DefinitionsItemBuilder {
|
|
||||||
exported: bool,
|
|
||||||
kind: Option<DefinitionsItemKind>,
|
|
||||||
typ: Option<String>,
|
|
||||||
name: Option<String>,
|
|
||||||
meta: Option<String>,
|
|
||||||
value: Option<String>,
|
|
||||||
children: Vec<DefinitionsItem>,
|
|
||||||
args: Vec<DefinitionsItemFunctionArg>,
|
|
||||||
rets: Vec<DefinitionsItemFunctionRet>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl DefinitionsItemBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
|
||||||
pub fn as_exported(mut self) -> Self {
|
|
||||||
self.exported = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_kind(mut self, kind: DefinitionsItemKind) -> Self {
|
|
||||||
self.kind = Some(kind);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_name<S: AsRef<str>>(mut self, name: S) -> Self {
|
|
||||||
self.name = Some(name.as_ref().to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_type(mut self, typ: String) -> Self {
|
|
||||||
self.typ = Some(typ);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_meta<S: AsRef<str>>(mut self, meta: S) -> Self {
|
|
||||||
self.meta = Some(meta.as_ref().to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_value<S: AsRef<str>>(mut self, value: S) -> Self {
|
|
||||||
self.value = Some(value.as_ref().to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_child(mut self, child: DefinitionsItem) -> Self {
|
|
||||||
self.children.push(child);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_children(mut self, children: &[DefinitionsItem]) -> Self {
|
|
||||||
self.children.extend_from_slice(children);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_arg(mut self, arg: DefinitionsItemFunctionArg) -> Self {
|
|
||||||
self.args.push(arg);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_args(mut self, args: &[DefinitionsItemFunctionArg]) -> Self {
|
|
||||||
for arg in args {
|
|
||||||
self.args.push(arg.clone());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_ret(mut self, ret: DefinitionsItemFunctionRet) -> Self {
|
|
||||||
self.rets.push(ret);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_rets(mut self, rets: &[DefinitionsItemFunctionRet]) -> Self {
|
|
||||||
for ret in rets {
|
|
||||||
self.rets.push(ret.clone());
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> Result<DefinitionsItem> {
|
|
||||||
if let Some(kind) = self.kind {
|
|
||||||
let mut children = self.children;
|
|
||||||
children.sort_by(|left, right| left.name.cmp(&right.name));
|
|
||||||
Ok(DefinitionsItem {
|
|
||||||
exported: self.exported,
|
|
||||||
kind,
|
|
||||||
typ: self.typ,
|
|
||||||
name: self.name,
|
|
||||||
meta: self.meta,
|
|
||||||
value: self.value,
|
|
||||||
children,
|
|
||||||
args: self.args,
|
|
||||||
rets: self.rets,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
bail!("Missing doc item kind")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&DefinitionsItem> for DefinitionsItemBuilder {
|
|
||||||
fn from(value: &DefinitionsItem) -> Self {
|
|
||||||
Self {
|
|
||||||
exported: value.exported,
|
|
||||||
kind: Some(value.kind),
|
|
||||||
typ: value.typ.clone(),
|
|
||||||
name: value.name.clone(),
|
|
||||||
meta: value.meta.clone(),
|
|
||||||
value: value.value.clone(),
|
|
||||||
children: value.children.clone(),
|
|
||||||
args: value.args.clone(),
|
|
||||||
rets: value.rets.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::kind::DefinitionsItemKind;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct DefinitionsItemFunctionArg {
|
|
||||||
pub name: String,
|
|
||||||
pub typedef: String,
|
|
||||||
pub typedef_simple: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefinitionsItemFunctionArg {
|
|
||||||
pub fn new<N, T, TS>(name: N, typedef: T, typedef_simple: TS) -> Self
|
|
||||||
where
|
|
||||||
N: Into<String>,
|
|
||||||
T: Into<String>,
|
|
||||||
TS: Into<String>,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
name: name.into(),
|
|
||||||
typedef: typedef.into(),
|
|
||||||
typedef_simple: typedef_simple.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct DefinitionsItemFunctionRet {
|
|
||||||
pub typedef: String,
|
|
||||||
pub typedef_simple: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefinitionsItemFunctionRet {
|
|
||||||
pub fn new<T, TS>(typedef: T, typedef_simple: TS) -> Self
|
|
||||||
where
|
|
||||||
T: Into<String>,
|
|
||||||
TS: Into<String>,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
typedef: typedef.into(),
|
|
||||||
typedef_simple: typedef_simple.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DefinitionsItem {
|
|
||||||
#[serde(skip_serializing_if = "skip_serialize_is_false")]
|
|
||||||
pub(super) exported: bool,
|
|
||||||
pub(super) kind: DefinitionsItemKind,
|
|
||||||
pub(super) typ: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub(super) name: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub(super) meta: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub(super) value: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub(super) children: Vec<DefinitionsItem>,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub(super) args: Vec<DefinitionsItemFunctionArg>,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub(super) rets: Vec<DefinitionsItemFunctionRet>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
|
||||||
fn skip_serialize_is_false(b: &bool) -> bool {
|
|
||||||
!b
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for DefinitionsItem {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
match self.kind.partial_cmp(&other.kind).unwrap() {
|
|
||||||
Ordering::Equal => {}
|
|
||||||
ord => return Some(ord),
|
|
||||||
}
|
|
||||||
match self.name.partial_cmp(&other.name).unwrap() {
|
|
||||||
Ordering::Equal => {}
|
|
||||||
ord => return Some(ord),
|
|
||||||
}
|
|
||||||
match (&self.value, &other.value) {
|
|
||||||
(Some(value_self), Some(value_other)) => {
|
|
||||||
match value_self.partial_cmp(value_other).unwrap() {
|
|
||||||
Ordering::Equal => {}
|
|
||||||
ord => return Some(ord),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(_), None) => return Some(Ordering::Less),
|
|
||||||
(None, Some(_)) => return Some(Ordering::Greater),
|
|
||||||
(None, None) => {}
|
|
||||||
}
|
|
||||||
Some(Ordering::Equal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl DefinitionsItem {
|
|
||||||
pub fn is_exported(&self) -> bool {
|
|
||||||
self.exported
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_root(&self) -> bool {
|
|
||||||
self.kind.is_root()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_type(&self) -> bool {
|
|
||||||
self.kind.is_type()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_table(&self) -> bool {
|
|
||||||
self.kind.is_table()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_property(&self) -> bool {
|
|
||||||
self.kind.is_property()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_function(&self) -> bool {
|
|
||||||
self.kind.is_function()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_description(&self) -> bool {
|
|
||||||
self.kind.is_description()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_tag(&self) -> bool {
|
|
||||||
self.kind.is_tag()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kind(&self) -> DefinitionsItemKind {
|
|
||||||
self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(&self) -> Option<&str> {
|
|
||||||
self.name.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_type(&self) -> Option<String> {
|
|
||||||
self.typ.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_meta(&self) -> Option<&str> {
|
|
||||||
self.meta.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_value(&self) -> Option<&str> {
|
|
||||||
self.value.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn children(&self) -> &[DefinitionsItem] {
|
|
||||||
&self.children
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn args(&self) -> Vec<&DefinitionsItemFunctionArg> {
|
|
||||||
self.args.iter().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rets(&self) -> Vec<&DefinitionsItemFunctionRet> {
|
|
||||||
self.rets.iter().collect()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DefinitionsItemKind {
|
|
||||||
Root,
|
|
||||||
Type,
|
|
||||||
Table,
|
|
||||||
Property,
|
|
||||||
Function,
|
|
||||||
Description,
|
|
||||||
Tag,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl DefinitionsItemKind {
|
|
||||||
pub fn is_root(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Root
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_type(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Type
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_table(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Table
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_property(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Property
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_function(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Function
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_description(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Description
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_tag(self) -> bool {
|
|
||||||
self == DefinitionsItemKind::Tag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for DefinitionsItemKind {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Self::Root => "Root",
|
|
||||||
Self::Type => "Type",
|
|
||||||
Self::Table => "Table",
|
|
||||||
Self::Property => "Property",
|
|
||||||
Self::Function => "Function",
|
|
||||||
Self::Description => "Description",
|
|
||||||
Self::Tag => "Tag",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
mod builder;
|
|
||||||
mod item;
|
|
||||||
mod kind;
|
|
||||||
mod moonwave;
|
|
||||||
mod parser;
|
|
||||||
mod tag;
|
|
||||||
mod tree;
|
|
||||||
mod type_info_ext;
|
|
||||||
|
|
||||||
pub use builder::DefinitionsItemBuilder;
|
|
||||||
pub use item::DefinitionsItem;
|
|
||||||
pub use kind::DefinitionsItemKind;
|
|
||||||
pub use tag::DefinitionsItemTag;
|
|
||||||
pub use tree::DefinitionsTree;
|
|
|
@ -1,85 +0,0 @@
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use super::{builder::DefinitionsItemBuilder, item::DefinitionsItem, kind::DefinitionsItemKind};
|
|
||||||
|
|
||||||
fn should_separate_tag_meta(tag_kind: &str) -> bool {
|
|
||||||
matches!(tag_kind.trim().to_ascii_lowercase().as_ref(), "param")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_moonwave_style_tag(line: &str) -> Option<DefinitionsItem> {
|
|
||||||
let tag_regex = Regex::new(r#"^@(\S+)\s*(.*)$"#).unwrap();
|
|
||||||
if tag_regex.is_match(line) {
|
|
||||||
let captures = tag_regex.captures(line).unwrap();
|
|
||||||
let tag_kind = captures.get(1).unwrap().as_str();
|
|
||||||
let tag_rest = captures.get(2).unwrap().as_str();
|
|
||||||
let mut tag_words = tag_rest.split_whitespace().collect::<Vec<_>>();
|
|
||||||
let tag_name = if !tag_words.is_empty() && should_separate_tag_meta(tag_kind) {
|
|
||||||
tag_words.remove(0).to_string()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
let tag_contents = tag_words.join(" ");
|
|
||||||
if tag_kind.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let mut builder = DefinitionsItemBuilder::new()
|
|
||||||
.with_kind(DefinitionsItemKind::Tag)
|
|
||||||
.with_name(tag_kind);
|
|
||||||
if !tag_name.is_empty() {
|
|
||||||
builder = builder.with_meta(tag_name);
|
|
||||||
}
|
|
||||||
if !tag_contents.is_empty() {
|
|
||||||
builder = builder.with_value(tag_contents);
|
|
||||||
}
|
|
||||||
Some(builder.build().unwrap())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn parse_moonwave_style_comment(comment: &str) -> Vec<DefinitionsItem> {
|
|
||||||
let no_tabs = comment.replace('\t', " ");
|
|
||||||
let lines = no_tabs.split('\n').collect::<Vec<_>>();
|
|
||||||
let indent_len =
|
|
||||||
lines.iter().fold(usize::MAX, |acc, line| {
|
|
||||||
let first = line.chars().enumerate().find_map(|(idx, ch)| {
|
|
||||||
if ch.is_whitespace() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(idx)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(first_non_whitespace) = first {
|
|
||||||
acc.min(first_non_whitespace)
|
|
||||||
} else {
|
|
||||||
acc
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let unindented_lines = lines.iter().map(|line| {
|
|
||||||
if line.chars().any(|c| !c.is_whitespace()) {
|
|
||||||
&line[indent_len..]
|
|
||||||
} else {
|
|
||||||
line
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut doc_items = Vec::new();
|
|
||||||
let mut doc_lines = Vec::new();
|
|
||||||
for line in unindented_lines {
|
|
||||||
if let Some(tag) = parse_moonwave_style_tag(line) {
|
|
||||||
doc_items.push(tag);
|
|
||||||
} else {
|
|
||||||
doc_lines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !doc_lines.is_empty() {
|
|
||||||
doc_items.push(
|
|
||||||
DefinitionsItemBuilder::new()
|
|
||||||
.with_kind(DefinitionsItemKind::Description)
|
|
||||||
.with_value(doc_lines.join("\n"))
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
doc_items
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use full_moon::{
|
|
||||||
ast::{
|
|
||||||
types::{TypeFieldKey, TypeInfo},
|
|
||||||
Stmt,
|
|
||||||
},
|
|
||||||
tokenizer::{TokenReference, TokenType},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
builder::DefinitionsItemBuilder, item::DefinitionsItem, moonwave::parse_moonwave_style_comment,
|
|
||||||
type_info_ext::TypeInfoExt, DefinitionsItemKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct DefinitionsParserItem {
|
|
||||||
name: String,
|
|
||||||
comment: Option<String>,
|
|
||||||
type_info: TypeInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DefinitionsParser {
|
|
||||||
found_top_level_items: BTreeMap<String, DefinitionsParserItem>,
|
|
||||||
found_top_level_types: HashMap<String, TypeInfo>,
|
|
||||||
found_top_level_comments: HashMap<String, Option<String>>,
|
|
||||||
found_top_level_exports: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefinitionsParser {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
found_top_level_items: BTreeMap::new(),
|
|
||||||
found_top_level_types: HashMap::new(),
|
|
||||||
found_top_level_comments: HashMap::new(),
|
|
||||||
found_top_level_exports: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Parses the given Luau type definitions into parser items.
|
|
||||||
|
|
||||||
The parser items will be stored internally and can be converted
|
|
||||||
into usable definition items using [`DefinitionsParser::drain`].
|
|
||||||
*/
|
|
||||||
pub fn parse<S>(&mut self, contents: S) -> Result<()>
|
|
||||||
where
|
|
||||||
S: AsRef<str>,
|
|
||||||
{
|
|
||||||
// Parse contents into top-level parser items for later use
|
|
||||||
let mut found_top_level_items = BTreeMap::new();
|
|
||||||
let mut found_top_level_types = HashMap::new();
|
|
||||||
let mut found_top_level_comments = HashMap::new();
|
|
||||||
let mut found_top_level_exports = HashSet::new();
|
|
||||||
let ast =
|
|
||||||
full_moon::parse(contents.as_ref()).context("Failed to parse type definitions")?;
|
|
||||||
for stmt in ast.nodes().stmts() {
|
|
||||||
if let Some((exported, declaration, token_reference)) = match stmt {
|
|
||||||
Stmt::ExportedTypeDeclaration(exp) => {
|
|
||||||
Some((true, exp.type_declaration(), exp.export_token()))
|
|
||||||
}
|
|
||||||
Stmt::TypeDeclaration(typ) => Some((false, typ, typ.type_token())),
|
|
||||||
_ => None,
|
|
||||||
} {
|
|
||||||
let name = declaration.type_name().token().to_string();
|
|
||||||
let comment = find_token_moonwave_comment(token_reference);
|
|
||||||
found_top_level_items.insert(
|
|
||||||
name.clone(),
|
|
||||||
DefinitionsParserItem {
|
|
||||||
name: name.clone(),
|
|
||||||
comment: comment.clone(),
|
|
||||||
type_info: declaration.type_definition().clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
found_top_level_types.insert(name.clone(), declaration.type_definition().clone());
|
|
||||||
found_top_level_comments.insert(name.clone(), comment);
|
|
||||||
if exported {
|
|
||||||
found_top_level_exports.insert(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Store results
|
|
||||||
self.found_top_level_items = found_top_level_items;
|
|
||||||
self.found_top_level_types = found_top_level_types;
|
|
||||||
self.found_top_level_comments = found_top_level_comments;
|
|
||||||
self.found_top_level_exports = found_top_level_exports.into_iter().collect();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_parser_item_into_doc_item(
|
|
||||||
&self,
|
|
||||||
item: DefinitionsParserItem,
|
|
||||||
kind: Option<DefinitionsItemKind>,
|
|
||||||
) -> DefinitionsItem {
|
|
||||||
let mut builder = DefinitionsItemBuilder::new()
|
|
||||||
.with_kind(kind.unwrap_or_else(|| item.type_info.parse_definitions_kind()))
|
|
||||||
.with_name(&item.name)
|
|
||||||
.with_type(item.type_info.to_string());
|
|
||||||
if self.found_top_level_exports.contains(&item.name) {
|
|
||||||
builder = builder.as_exported();
|
|
||||||
}
|
|
||||||
if let Some(comment) = item.comment {
|
|
||||||
builder = builder.with_children(&parse_moonwave_style_comment(&comment));
|
|
||||||
}
|
|
||||||
if let Some(args) = item
|
|
||||||
.type_info
|
|
||||||
.extract_args_normalized(&self.found_top_level_types)
|
|
||||||
{
|
|
||||||
builder = builder.with_args(&args);
|
|
||||||
}
|
|
||||||
if let TypeInfo::Table { fields, .. } = item.type_info {
|
|
||||||
for field in fields.iter() {
|
|
||||||
if let TypeFieldKey::Name(name) = field.key() {
|
|
||||||
builder = builder.with_child(self.convert_parser_item_into_doc_item(
|
|
||||||
DefinitionsParserItem {
|
|
||||||
name: name.token().to_string(),
|
|
||||||
comment: find_token_moonwave_comment(name),
|
|
||||||
type_info: field.value().clone(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.build().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Converts currently stored parser items into definition items.
|
|
||||||
|
|
||||||
This will consume (drain) all stored parser items, leaving the parser empty.
|
|
||||||
*/
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
|
||||||
pub fn drain(&mut self) -> Result<Vec<DefinitionsItem>> {
|
|
||||||
let mut resulting_items = Vec::new();
|
|
||||||
for top_level_item in self.found_top_level_items.values() {
|
|
||||||
resulting_items
|
|
||||||
.push(self.convert_parser_item_into_doc_item(top_level_item.clone(), None));
|
|
||||||
}
|
|
||||||
self.found_top_level_items = BTreeMap::new();
|
|
||||||
self.found_top_level_types = HashMap::new();
|
|
||||||
self.found_top_level_comments = HashMap::new();
|
|
||||||
self.found_top_level_exports = Vec::new();
|
|
||||||
Ok(resulting_items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_token_moonwave_comment(token: &TokenReference) -> Option<String> {
|
|
||||||
token
|
|
||||||
.leading_trivia()
|
|
||||||
.filter_map(|trivia| match trivia.token_type() {
|
|
||||||
TokenType::MultiLineComment { blocks, comment } if blocks == &1 => Some(comment),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.last()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::item::DefinitionsItem;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DefinitionsItemTag {
|
|
||||||
Class(String),
|
|
||||||
Type(String),
|
|
||||||
Within(String),
|
|
||||||
Param((String, String)),
|
|
||||||
Return(String),
|
|
||||||
MustUse,
|
|
||||||
ReadOnly,
|
|
||||||
ReadWrite,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl DefinitionsItemTag {
|
|
||||||
pub fn is_class(&self) -> bool {
|
|
||||||
matches!(self, Self::Class(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_type(&self) -> bool {
|
|
||||||
matches!(self, Self::Class(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_within(&self) -> bool {
|
|
||||||
matches!(self, Self::Within(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_param(&self) -> bool {
|
|
||||||
matches!(self, Self::Param(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_return(&self) -> bool {
|
|
||||||
matches!(self, Self::Return(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_must_use(&self) -> bool {
|
|
||||||
self == &Self::MustUse
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_read_only(&self) -> bool {
|
|
||||||
self == &Self::ReadOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_read_write(&self) -> bool {
|
|
||||||
self == &Self::ReadWrite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&DefinitionsItem> for DefinitionsItemTag {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
fn try_from(value: &DefinitionsItem) -> Result<Self> {
|
|
||||||
if let Some(name) = value.get_name() {
|
|
||||||
Ok(match name.trim().to_ascii_lowercase().as_ref() {
|
|
||||||
"class" => Self::Class(
|
|
||||||
value
|
|
||||||
.get_value()
|
|
||||||
.context("Missing class name for class tag")?
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
"type" => Self::Class(
|
|
||||||
value
|
|
||||||
.get_value()
|
|
||||||
.context("Missing type name for type tag")?
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
"within" => Self::Within(
|
|
||||||
value
|
|
||||||
.get_value()
|
|
||||||
.context("Missing class name for within tag")?
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
"param" => Self::Param((
|
|
||||||
value
|
|
||||||
.get_meta()
|
|
||||||
.context("Missing param name for param tag")?
|
|
||||||
.to_string(),
|
|
||||||
value
|
|
||||||
.get_value()
|
|
||||||
.context("Missing param value for param tag")?
|
|
||||||
.to_string(),
|
|
||||||
)),
|
|
||||||
"return" => Self::Return(
|
|
||||||
value
|
|
||||||
.get_value()
|
|
||||||
.context("Missing description for return tag")?
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
"must_use" => Self::MustUse,
|
|
||||||
"read_only" => Self::ReadOnly,
|
|
||||||
"read_write" => Self::ReadWrite,
|
|
||||||
s => bail!("Unknown docs tag: '{}'", s),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
bail!("Doc item has no name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
builder::DefinitionsItemBuilder, item::DefinitionsItem, kind::DefinitionsItemKind,
|
|
||||||
parser::DefinitionsParser,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct DefinitionsTree(DefinitionsItem);
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl DefinitionsTree {
|
|
||||||
pub fn from_type_definitions<S: AsRef<str>>(type_definitions_contents: S) -> Result<Self> {
|
|
||||||
let mut parser = DefinitionsParser::new();
|
|
||||||
parser
|
|
||||||
.parse(type_definitions_contents)
|
|
||||||
.context("Failed to parse type definitions AST")?;
|
|
||||||
let top_level_definition_items = parser
|
|
||||||
.drain()
|
|
||||||
.context("Failed to convert parser items into definition items")?;
|
|
||||||
let root = DefinitionsItemBuilder::new()
|
|
||||||
.with_kind(DefinitionsItemKind::Root)
|
|
||||||
.with_name("<<<ROOT>>>")
|
|
||||||
.with_children(&top_level_definition_items)
|
|
||||||
.build()?;
|
|
||||||
Ok(Self(root))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::unused_self)]
|
|
||||||
pub fn is_root(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_inner(self) -> DefinitionsItem {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for DefinitionsTree {
|
|
||||||
type Target = DefinitionsItem;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for DefinitionsTree {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,333 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use full_moon::{
|
|
||||||
ast::types::{TypeArgument, TypeInfo},
|
|
||||||
tokenizer::{Symbol, Token, TokenReference, TokenType},
|
|
||||||
ShortString,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
item::{DefinitionsItemFunctionArg, DefinitionsItemFunctionRet},
|
|
||||||
kind::DefinitionsItemKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) trait TypeInfoExt {
|
|
||||||
fn is_fn(&self) -> bool;
|
|
||||||
fn parse_definitions_kind(&self) -> DefinitionsItemKind;
|
|
||||||
fn stringify_simple(
|
|
||||||
&self,
|
|
||||||
parent_typ: Option<&TypeInfo>,
|
|
||||||
type_lookup_table: &HashMap<String, TypeInfo>,
|
|
||||||
) -> String;
|
|
||||||
fn extract_args(&self) -> Vec<TypeArgument>;
|
|
||||||
fn extract_args_normalized(
|
|
||||||
&self,
|
|
||||||
type_lookup_table: &HashMap<String, TypeInfo>,
|
|
||||||
) -> Option<Vec<DefinitionsItemFunctionArg>>;
|
|
||||||
// fn extract_rets(&self) -> Vec<TypeArgument>;
|
|
||||||
// fn extract_rets_normalized(
|
|
||||||
// &self,
|
|
||||||
// type_lookup_table: &HashMap<String, TypeInfo>,
|
|
||||||
// ) -> Option<Vec<DefinitionsItemFunctionRet>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TypeInfoExt for TypeInfo {
|
|
||||||
/**
|
|
||||||
Checks if this type represents a function or not.
|
|
||||||
|
|
||||||
If the type is a tuple, union, or intersection, it will be checked recursively.
|
|
||||||
*/
|
|
||||||
fn is_fn(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
TypeInfo::Callback { .. } => true,
|
|
||||||
TypeInfo::Tuple { types, .. } => types.iter().all(Self::is_fn),
|
|
||||||
TypeInfo::Union { left, right, .. } | TypeInfo::Intersection { left, right, .. } => {
|
|
||||||
left.is_fn() || right.is_fn()
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Parses the definitions item kind from the type.
|
|
||||||
|
|
||||||
If the type is a tupe, union, or intersection, all the inner types
|
|
||||||
are required to be equivalent in terms of definitions item kinds.
|
|
||||||
*/
|
|
||||||
fn parse_definitions_kind(&self) -> DefinitionsItemKind {
|
|
||||||
match self {
|
|
||||||
TypeInfo::Array { .. } | TypeInfo::Table { .. } => DefinitionsItemKind::Table,
|
|
||||||
TypeInfo::Basic(_) | TypeInfo::String(_) => DefinitionsItemKind::Property,
|
|
||||||
TypeInfo::Optional { base, .. } => Self::parse_definitions_kind(base.as_ref()),
|
|
||||||
TypeInfo::Tuple { types, .. } => {
|
|
||||||
let mut kinds = types
|
|
||||||
.iter()
|
|
||||||
.map(Self::parse_definitions_kind)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let kinds_all_the_same = kinds.windows(2).all(|w| w[0] == w[1]);
|
|
||||||
if kinds_all_the_same && !kinds.is_empty() {
|
|
||||||
kinds.pop().unwrap()
|
|
||||||
} else {
|
|
||||||
unimplemented!(
|
|
||||||
"Missing support for tuple with differing types in type definitions parser",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TypeInfo::Union { left, right, .. } | TypeInfo::Intersection { left, right, .. } => {
|
|
||||||
let kind_left = Self::parse_definitions_kind(left.as_ref());
|
|
||||||
let kind_right = Self::parse_definitions_kind(right.as_ref());
|
|
||||||
if kind_left == kind_right {
|
|
||||||
kind_left
|
|
||||||
} else {
|
|
||||||
unimplemented!(
|
|
||||||
"Missing support for union/intersection with differing types in type definitions parser",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
typ if typ.is_fn() => DefinitionsItemKind::Function,
|
|
||||||
typ => unimplemented!(
|
|
||||||
"Missing support for TypeInfo in type definitions parser:\n{}",
|
|
||||||
typ.to_string()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Stringifies the type into a simplified type string.
|
|
||||||
|
|
||||||
The simplified type string match one of the following formats:
|
|
||||||
|
|
||||||
* `any`
|
|
||||||
* `boolean`
|
|
||||||
* `string`
|
|
||||||
* `function`
|
|
||||||
* `table`
|
|
||||||
* `CustomTypeName`
|
|
||||||
* `TypeName?`
|
|
||||||
* `TypeName | OtherTypeName`
|
|
||||||
* `{ TypeName }`
|
|
||||||
* `"string-literal"`
|
|
||||||
*/
|
|
||||||
fn stringify_simple(
|
|
||||||
&self,
|
|
||||||
parent_typ: Option<&TypeInfo>,
|
|
||||||
type_lookup_table: &HashMap<String, TypeInfo>,
|
|
||||||
) -> String {
|
|
||||||
match self {
|
|
||||||
TypeInfo::Array { type_info, .. } => {
|
|
||||||
format!(
|
|
||||||
"{{ {} }}",
|
|
||||||
type_info
|
|
||||||
.as_ref()
|
|
||||||
.stringify_simple(Some(self), type_lookup_table)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TypeInfo::Basic(tok) => {
|
|
||||||
let tok_str = tok.token().to_string().trim().to_string();
|
|
||||||
let mut any_str = None;
|
|
||||||
// If the function that contains this arg has generic and a
|
|
||||||
// generic is the same as this token, we stringify it as any
|
|
||||||
if let Some(parent) = parent_typ {
|
|
||||||
if let Some(TypeInfo::Callback {
|
|
||||||
generics: Some(callback_generics),
|
|
||||||
..
|
|
||||||
}) = try_extract_callback_type_info(parent)
|
|
||||||
{
|
|
||||||
if callback_generics
|
|
||||||
.generics()
|
|
||||||
.iter()
|
|
||||||
.any(|g| g.to_string() == tok_str)
|
|
||||||
{
|
|
||||||
any_str = Some("any".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also check if we got a referenced type, meaning that it
|
|
||||||
// exists in the lookup table of global types passed to us
|
|
||||||
if let Some(any_str) = any_str {
|
|
||||||
any_str
|
|
||||||
} else if let Some(referenced_typ) = type_lookup_table.get(&tok_str) {
|
|
||||||
referenced_typ.stringify_simple(None, type_lookup_table)
|
|
||||||
} else {
|
|
||||||
tok_str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TypeInfo::String(str) => str.token().to_string(),
|
|
||||||
TypeInfo::Boolean(_) => "boolean".to_string(),
|
|
||||||
TypeInfo::Callback { .. } => "function".to_string(),
|
|
||||||
TypeInfo::Optional { base, .. } => {
|
|
||||||
format!(
|
|
||||||
"{}?",
|
|
||||||
base.as_ref()
|
|
||||||
.stringify_simple(Some(self), type_lookup_table)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TypeInfo::Table { .. } => "table".to_string(),
|
|
||||||
TypeInfo::Union { left, right, .. } => {
|
|
||||||
format!(
|
|
||||||
"{} {} {}",
|
|
||||||
left.as_ref()
|
|
||||||
.stringify_simple(Some(self), type_lookup_table),
|
|
||||||
Symbol::Pipe,
|
|
||||||
right
|
|
||||||
.as_ref()
|
|
||||||
.stringify_simple(Some(self), type_lookup_table)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// FUTURE: Is there any other type that we can
|
|
||||||
// stringify to a primitive in an obvious way?
|
|
||||||
_ => "...".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_args(&self) -> Vec<TypeArgument> {
|
|
||||||
if self.is_fn() {
|
|
||||||
match self {
|
|
||||||
TypeInfo::Callback { arguments, .. } => {
|
|
||||||
arguments.iter().cloned().collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
TypeInfo::Tuple { types, .. } => types
|
|
||||||
.iter()
|
|
||||||
.next()
|
|
||||||
.expect("Function tuple type was empty")
|
|
||||||
.extract_args(),
|
|
||||||
TypeInfo::Union { left, right, .. }
|
|
||||||
| TypeInfo::Intersection { left, right, .. } => {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
result = merge_type_argument_vecs(result, left.extract_args());
|
|
||||||
result = merge_type_argument_vecs(result, right.extract_args());
|
|
||||||
result
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_args_normalized(
|
|
||||||
&self,
|
|
||||||
type_lookup_table: &HashMap<String, TypeInfo>,
|
|
||||||
) -> Option<Vec<DefinitionsItemFunctionArg>> {
|
|
||||||
if self.is_fn() {
|
|
||||||
let args_stringified_not_normalized = self
|
|
||||||
.extract_args()
|
|
||||||
.iter()
|
|
||||||
.map(|type_arg| {
|
|
||||||
(
|
|
||||||
type_arg
|
|
||||||
.name()
|
|
||||||
.map_or_else(|| "_".to_string(), |n| n.0.to_string()),
|
|
||||||
type_arg.type_info().to_string(),
|
|
||||||
type_arg
|
|
||||||
.type_info()
|
|
||||||
.stringify_simple(Some(self), type_lookup_table),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let mut args = Vec::new();
|
|
||||||
for (arg_name, arg_typedef, arg_typedef_simplified) in args_stringified_not_normalized {
|
|
||||||
args.push(DefinitionsItemFunctionArg::new(
|
|
||||||
arg_name,
|
|
||||||
arg_typedef,
|
|
||||||
normalize_type(&arg_typedef_simplified),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Some(args)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_extract_callback_type_info(type_info: &TypeInfo) -> Option<&TypeInfo> {
|
|
||||||
match type_info {
|
|
||||||
TypeInfo::Callback { .. } => Some(type_info),
|
|
||||||
TypeInfo::Tuple { types, .. } => types.iter().find_map(try_extract_callback_type_info),
|
|
||||||
TypeInfo::Union { left, right, .. } | TypeInfo::Intersection { left, right, .. } => {
|
|
||||||
try_extract_callback_type_info(left).or_else(|| try_extract_callback_type_info(right))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_empty_type_argument() -> TypeArgument {
|
|
||||||
TypeArgument::new(TypeInfo::Basic(TokenReference::new(
|
|
||||||
vec![],
|
|
||||||
Token::new(TokenType::Symbol {
|
|
||||||
symbol: Symbol::Nil,
|
|
||||||
}),
|
|
||||||
vec![],
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_type_arguments(left: TypeArgument, right: TypeArgument) -> TypeArgument {
|
|
||||||
TypeArgument::new(TypeInfo::Union {
|
|
||||||
left: Box::new(left.type_info().clone()),
|
|
||||||
pipe: TokenReference::new(
|
|
||||||
vec![Token::new(TokenType::Whitespace {
|
|
||||||
characters: ShortString::new(" "),
|
|
||||||
})],
|
|
||||||
Token::new(TokenType::Symbol {
|
|
||||||
symbol: Symbol::Pipe,
|
|
||||||
}),
|
|
||||||
vec![Token::new(TokenType::Whitespace {
|
|
||||||
characters: ShortString::new(" "),
|
|
||||||
})],
|
|
||||||
),
|
|
||||||
right: Box::new(right.type_info().clone()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_type_argument_vecs(
|
|
||||||
existing: Vec<TypeArgument>,
|
|
||||||
new: Vec<TypeArgument>,
|
|
||||||
) -> Vec<TypeArgument> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for (index, argument) in new.iter().enumerate() {
|
|
||||||
if let Some(existing) = existing.get(index) {
|
|
||||||
result.push(merge_type_arguments(existing.clone(), argument.clone()));
|
|
||||||
} else {
|
|
||||||
result.push(merge_type_arguments(
|
|
||||||
make_empty_type_argument(),
|
|
||||||
argument.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_type(simplified: &str) -> String {
|
|
||||||
let separator = format!(" {} ", Symbol::Pipe);
|
|
||||||
let arg_parts = simplified.split(&separator).collect::<Vec<_>>();
|
|
||||||
// Check if we got any optional arg, if so then the entire possible
|
|
||||||
// union of args will be optional when merged together / normalized
|
|
||||||
let is_optional = arg_parts
|
|
||||||
.iter()
|
|
||||||
.any(|part| part == &"nil" || part.ends_with('?'));
|
|
||||||
// Get rid of any nils or optional markers since we keep track of it above
|
|
||||||
let mut arg_parts_no_nils = arg_parts
|
|
||||||
.iter()
|
|
||||||
.filter_map(|arg_part| {
|
|
||||||
if arg_part == &"nil" {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(arg_part.trim_end_matches('?'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
arg_parts_no_nils.sort_unstable(); // Sort the args to be able to dedup
|
|
||||||
arg_parts_no_nils.dedup(); // Deduplicate types that are the exact same shape
|
|
||||||
if is_optional {
|
|
||||||
if arg_parts_no_nils.len() > 1 {
|
|
||||||
// A union of args that is nillable should be enclosed in parens to make
|
|
||||||
// it more clear that the entire arg is nillable and not just the last type
|
|
||||||
format!("({})?", arg_parts_no_nils.join(&separator))
|
|
||||||
} else {
|
|
||||||
// Just one nillable arg, does not need any parens
|
|
||||||
format!("{}?", arg_parts_no_nils.first().unwrap())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
arg_parts_no_nils.join(&separator)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,240 +0,0 @@
|
||||||
use std::{collections::HashMap, fmt::Write, path::PathBuf};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
|
|
||||||
use futures_util::future::try_join_all;
|
|
||||||
use tokio::fs::{create_dir_all, write};
|
|
||||||
|
|
||||||
use super::definitions::{
|
|
||||||
DefinitionsItem, DefinitionsItemBuilder, DefinitionsItemKind, DefinitionsItemTag,
|
|
||||||
DefinitionsTree,
|
|
||||||
};
|
|
||||||
|
|
||||||
const GENERATED_COMMENT_TAG: &str = "<!-- @generated with lune-cli -->";
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub async fn generate_from_type_definitions(
|
|
||||||
definitions: HashMap<String, DefinitionsTree>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut dirs_to_write = Vec::new();
|
|
||||||
let mut files_to_write = Vec::new();
|
|
||||||
// Create the gitbook dir at the repo root
|
|
||||||
let path_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../../")
|
|
||||||
.canonicalize()
|
|
||||||
.unwrap();
|
|
||||||
let path_gitbook_dir = path_root.join("gitbook");
|
|
||||||
let path_gitbook_docs_dir = path_gitbook_dir.join("docs");
|
|
||||||
let path_gitbook_pages_dir = path_gitbook_docs_dir.join("pages");
|
|
||||||
let path_gitbook_api_dir = path_gitbook_pages_dir.join("api");
|
|
||||||
dirs_to_write.push(path_gitbook_dir.clone());
|
|
||||||
dirs_to_write.push(path_gitbook_docs_dir.clone());
|
|
||||||
dirs_to_write.push(path_gitbook_pages_dir.clone());
|
|
||||||
dirs_to_write.push(path_gitbook_api_dir.clone());
|
|
||||||
|
|
||||||
// Convert definition trees into single root items so that we can parse and write markdown recursively
|
|
||||||
let mut typedef_items = HashMap::new();
|
|
||||||
for (typedef_name, typedef_contents) in definitions {
|
|
||||||
let main = typedef_contents
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.find(
|
|
||||||
|c| matches!(c.get_name(), Some(s) if s.to_lowercase() == typedef_name.to_lowercase()),
|
|
||||||
)
|
|
||||||
.expect("Failed to find main export for generating typedef file");
|
|
||||||
|
|
||||||
let children = typedef_contents
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|child| {
|
|
||||||
if child == main {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
DefinitionsItemBuilder::from(child)
|
|
||||||
.with_kind(DefinitionsItemKind::Type)
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let root = DefinitionsItemBuilder::new()
|
|
||||||
.with_kind(main.kind())
|
|
||||||
.with_name(main.get_name().unwrap())
|
|
||||||
.with_children(main.children())
|
|
||||||
.with_children(&children);
|
|
||||||
let root_item = root.build().expect("Failed to build root definitions item");
|
|
||||||
|
|
||||||
typedef_items.insert(typedef_name.to_string(), root_item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate files for all subcategories
|
|
||||||
for (category_name, category_item) in typedef_items {
|
|
||||||
let path = path_gitbook_api_dir
|
|
||||||
.join(category_name.to_ascii_lowercase())
|
|
||||||
.with_extension("md");
|
|
||||||
let mut contents = String::new();
|
|
||||||
write!(contents, "{GENERATED_COMMENT_TAG}\n\n")?;
|
|
||||||
generate_markdown_documentation(&mut contents, &category_item, None, 0)?;
|
|
||||||
files_to_write.push((path, post_process_docs(contents)));
|
|
||||||
}
|
|
||||||
// Write all dirs and files only when we know generation was successful
|
|
||||||
let futs_dirs = dirs_to_write
|
|
||||||
.drain(..)
|
|
||||||
.map(create_dir_all)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let futs_files = files_to_write
|
|
||||||
.drain(..)
|
|
||||||
.map(|(path, contents)| write(path, contents))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
try_join_all(futs_dirs).await?;
|
|
||||||
try_join_all(futs_files).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_name(item: &DefinitionsItem) -> Result<String> {
|
|
||||||
item.children()
|
|
||||||
.iter()
|
|
||||||
.find_map(|child| {
|
|
||||||
if child.is_tag() {
|
|
||||||
if let Ok(DefinitionsItemTag::Class(c)) = DefinitionsItemTag::try_from(child) {
|
|
||||||
Some(c)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_else(|| item.get_name().map(ToString::to_string))
|
|
||||||
.context("Definitions item is missing a name")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn generate_markdown_documentation(
|
|
||||||
contents: &mut String,
|
|
||||||
item: &DefinitionsItem,
|
|
||||||
parent: Option<&DefinitionsItem>,
|
|
||||||
depth: usize,
|
|
||||||
) -> Result<()> {
|
|
||||||
match item.kind() {
|
|
||||||
DefinitionsItemKind::Type
|
|
||||||
| DefinitionsItemKind::Table
|
|
||||||
| DefinitionsItemKind::Property
|
|
||||||
| DefinitionsItemKind::Function => {
|
|
||||||
write!(
|
|
||||||
contents,
|
|
||||||
"\n{} {}\n",
|
|
||||||
if item.is_table() { "#" } else { "###" },
|
|
||||||
get_name(item)?
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
DefinitionsItemKind::Description => {
|
|
||||||
let desc = item.get_value().context("Description is missing a value")?;
|
|
||||||
write!(
|
|
||||||
contents,
|
|
||||||
"\n{}\n",
|
|
||||||
if depth >= 2 {
|
|
||||||
// HACK: We know our typedefs are formatted like this and
|
|
||||||
// it looks nicer to have this bolding instead of two
|
|
||||||
// headers using "###" in the function definition
|
|
||||||
desc.replace("### Example usage", "**Example usage:**")
|
|
||||||
} else {
|
|
||||||
desc.to_string()
|
|
||||||
}
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if item.is_function() && !item.args().is_empty() {
|
|
||||||
let args = item
|
|
||||||
.args()
|
|
||||||
.iter()
|
|
||||||
.map(|arg| format!("{}: {}", arg.name.trim(), arg.typedef.trim()))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
.replace("_: T...", "T...");
|
|
||||||
let func_name = item.get_name().unwrap_or("_");
|
|
||||||
let parent_name = parent.unwrap().get_name().unwrap_or("_");
|
|
||||||
let parent_pre = if parent_name.to_lowercase() == "uncategorized" {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!("{parent_name}.")
|
|
||||||
};
|
|
||||||
write!(
|
|
||||||
contents,
|
|
||||||
"\n```lua\nfunction {parent_pre}{func_name}({args})\n```\n",
|
|
||||||
)?;
|
|
||||||
} else if item.is_type() {
|
|
||||||
write!(
|
|
||||||
contents,
|
|
||||||
"\n```lua\ntype {} = {}\n```\n",
|
|
||||||
item.get_name().unwrap_or("_"),
|
|
||||||
item.get_type().unwrap_or_else(|| "{}".to_string()).trim()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
let descriptions = item
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.filter(|child| child.is_description())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let properties = item
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.filter(|child| child.is_property())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let functions = item
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.filter(|child| child.is_function())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let types = item
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.filter(|child| child.is_type())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for description in descriptions {
|
|
||||||
generate_markdown_documentation(contents, description, Some(item), depth + 1)?;
|
|
||||||
}
|
|
||||||
if !item.is_type() {
|
|
||||||
if !properties.is_empty() {
|
|
||||||
write!(contents, "\n\n---\n\n## Properties\n\n")?;
|
|
||||||
}
|
|
||||||
for property in properties {
|
|
||||||
generate_markdown_documentation(contents, property, Some(item), depth + 1)?;
|
|
||||||
}
|
|
||||||
if !functions.is_empty() {
|
|
||||||
write!(contents, "\n\n---\n\n## Functions\n\n")?;
|
|
||||||
}
|
|
||||||
for function in functions {
|
|
||||||
generate_markdown_documentation(contents, function, Some(item), depth + 1)?;
|
|
||||||
}
|
|
||||||
if !types.is_empty() {
|
|
||||||
write!(contents, "\n\n---\n\n## Types\n\n")?;
|
|
||||||
}
|
|
||||||
for typ in types {
|
|
||||||
generate_markdown_documentation(contents, typ, Some(item), depth + 1)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn post_process_docs(contents: String) -> String {
|
|
||||||
let no_empty_lines = contents
|
|
||||||
.lines()
|
|
||||||
.map(|line| {
|
|
||||||
if line.chars().all(char::is_whitespace) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
line
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
no_empty_lines
|
|
||||||
.replace("\n\n---", "\n---")
|
|
||||||
.replace("\n\n\n", "\n\n")
|
|
||||||
.replace("\n\n\n", "\n\n")
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use include_dir::Dir;
|
|
||||||
|
|
||||||
use self::definitions::DefinitionsTree;
|
|
||||||
|
|
||||||
mod gitbook_dir;
|
|
||||||
mod typedef_files;
|
|
||||||
|
|
||||||
pub mod definitions;
|
|
||||||
|
|
||||||
pub async fn generate_gitbook_dir_from_definitions(dir: &Dir<'_>) -> Result<()> {
|
|
||||||
let definitions = read_typedefs_dir(dir)?;
|
|
||||||
gitbook_dir::generate_from_type_definitions(definitions).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
|
|
||||||
let contents = read_typedefs_dir_contents(dir);
|
|
||||||
typedef_files::generate_from_type_definitions(contents).await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
|
|
||||||
let mut definitions = HashMap::new();
|
|
||||||
|
|
||||||
for entry in dir.find("*.luau").unwrap() {
|
|
||||||
let entry_file = entry.as_file().unwrap();
|
|
||||||
let entry_name = entry_file.path().file_name().unwrap().to_string_lossy();
|
|
||||||
|
|
||||||
let typedef_name = entry_name.trim_end_matches(".luau");
|
|
||||||
let typedef_contents = entry_file.contents().to_vec();
|
|
||||||
|
|
||||||
definitions.insert(typedef_name.to_string(), typedef_contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
definitions
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_typedefs_dir(dir: &Dir<'_>) -> Result<HashMap<String, DefinitionsTree>> {
|
|
||||||
let mut definitions = HashMap::new();
|
|
||||||
|
|
||||||
for entry in dir.find("*.luau").unwrap() {
|
|
||||||
let entry_file = entry.as_file().unwrap();
|
|
||||||
let entry_name = entry_file.path().file_name().unwrap().to_string_lossy();
|
|
||||||
|
|
||||||
let typedef_name = entry_name.trim_end_matches(".luau");
|
|
||||||
let typedef_contents = entry_file.contents_utf8().unwrap().to_string();
|
|
||||||
|
|
||||||
let typedef_tree = DefinitionsTree::from_type_definitions(&typedef_contents)?;
|
|
||||||
definitions.insert(typedef_name.to_string(), typedef_tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(definitions)
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use super::gen::generate_typedef_files_from_definitions;
|
use super::gen::generate_typedef_files_from_definitions;
|
||||||
|
|
||||||
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("docs/typedefs");
|
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("types");
|
||||||
|
|
||||||
pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode";
|
pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode";
|
||||||
pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases";
|
pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases";
|
Loading…
Reference in a new issue