diff --git a/.gitignore b/.gitignore deleted file mode 100644 index bbacba7..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -references/ -ts.txt -*.log \ No newline at end of file diff --git a/api-tests/test.py b/api-tests/test.py deleted file mode 100644 index 603f8ac..0000000 --- a/api-tests/test.py +++ /dev/null @@ -1,49 +0,0 @@ -import requests as req - - -class CreateItemChecks: - def createAuthor (this, result): - return 'response' in result - - def invalidType (this, result): - return 'response' in result - - -class CreateItemActions: - def createAuthor (this): - return req.post( - "http://127.0.0.1:8080/api/v/1.0/create-item?response_format=json", - json={ - "type" : "author", - "name" : "TestAuthor" - } - ).json() - - def invalidType (this): - return req.post( - "http://127.0.0.1:8080/api/v/1.0/create-item?response_format=json", - json={ - "type" : "asdasdsad", - "name" : "TestAuthor" - } - ).json() - -createItemActions = CreateItemActions() -createItemChecks = CreateItemChecks() - -def test (id, action, isDone, nextTest=lambda:None): - try: - print("""Test {id} runned..""") - result = action() - if isDone(result): - print("""Test {id} success!""") - nextTest() - else: - print("""Test {id} ERROR""") - print(result) - except Exception as exc: - print(exc) - -test_3 = lambda : test(3, createItemActions.createAuthor, createItemChecks.createAuthor) -test_2 = lambda : test(2, createItemActions.createAuthor, createItemChecks.createAuthor) -test_1 = test(1, createItemActions.invalidType, createItemChecks.invalidType, test) \ No newline at end of file diff --git a/api/index.js b/api/index.js deleted file mode 100644 index e262ca7..0000000 --- a/api/index.js +++ /dev/null @@ -1,4 +0,0 @@ -const router = require("express").Router(); -router.use("/v/1.0", require("./v1")); - -module.exports = router; \ No newline at end of file diff --git a/api/v1/errorClass.js b/api/v1/errorClass.js deleted file mode 100644 index 2c4db01..0000000 --- a/api/v1/errorClass.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = class ApiError extends Error { - constructor(message, details = {}, ...args) { - super(message, ...args); - this.message = message; - this.details = details; - this.name = "ApiError"; - } -}; \ No newline at end of file diff --git a/api/v1/index.js b/api/v1/index.js deleted file mode 100644 index 76ece4a..0000000 --- a/api/v1/index.js +++ /dev/null @@ -1,58 +0,0 @@ -const router = require("express").Router(); -const response = require("./response-wrapper"); -// const config = require('../../config-handler'); - -// Парсинг куки -//router.use(require('cookie-parser')()); - -// Загрузка музыки при помощи спец. метода -function muzicLoad(req, res) { - res.setHeader("Content-Type", "audio/mpeg"); - global.database.muzic.get( - (err, data) => { - data = data[0]?.data; - if (err) { - res.send(Buffer.from([])); - } else { - res.send(!data ? Buffer.from([]) : data); - } - }, - { where: { id: !Number.isInteger(+req.query.id) ? 0 : +req.query.id } }, - ); -} - -// Подгрузка с файла -router.use("/:method_name", async (req, res, next, ...etc) => { - if (req.params.method_name === "muzic") { - muzicLoad(req, res); - return; - } - - try { - const methodFunct = require(`./methods/${req.params.method_name}`); - response(methodFunct, req, res); - } catch (e) { - if (e.message.includes("Cannot find module")) { - const ApiError = require("./errorClass"); - res.status(400).sendModed( - await response( - (req, res) => { - throw new ApiError("METHOD_NOT_FOUNDED"); - }, - req, - res, - ), - ); - } else { - response( - async () => { - throw e; - }, - req, - res, - ); - } - } -}); - -module.exports = router; \ No newline at end of file diff --git a/api/v1/methods/create-item.js b/api/v1/methods/create-item.js deleted file mode 100644 index 15e31cb..0000000 --- a/api/v1/methods/create-item.js +++ /dev/null @@ -1,87 +0,0 @@ -const ApiError = require("../errorClass"); -const config = require("../../../config-handler"); - -async function isAuthorExists(authorId) { - if (!authorId) return false; - return ( - ( - await global.database.authors - .promiseMode() - .get({ where: { id: authorId } }) - ).length !== 0 - ); -} - -async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля type. - ["author", "muzic"].indexOf(req.json?.type) !== -1 && // Проверка поля name. - typeof req.json.name === "string" && - req.json.name.length >= 2 && - (req.json.type === "muzic" - ? true - : config().authors_blacklist.filter((blacklisted) => { - return req.json.name - .toLowerCase() - .includes(blacklisted.toLowerCase()); - }).length === 0) && // Дополнительные поля для muzic - (req.json.type === "muzic" - ? !!( - // Для `muzic` - ( - // Проверка поля author_id. - (await isAuthorExists(req.json.author_id)) && // Проверка поля data. (Передаётся либо ничего, либо строка с base64) - (req.json.data === undefined - ? true - : typeof req.json.data === "string") - ) - ) - : true) - ); -} - -module.exports = async (req, res) => { - if (req.json === undefined) { - // console.log(req.headers); - throw new ApiError("METHOD_MUST_BE_POST_JSON", { - request_method: req.method, - "content-type": !req.headers["content-type"] - ? null - : req.headers["content-type"], - }); - } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - type: req.json?.type === undefined ? null : req.json.type, - name: req.json?.name === undefined ? null : req.json.name, - ...(req.json?.type === "muzic" - ? { - author_id: - req.json?.author_id === undefined ? null : req.json?.author_id, - data: req.json?.data === undefined ? null : req.json.data, - } - : {}), - }, - }); - } - if (req.json.type === "author") { - let result = await global.database.authors.promiseMode().add({ - name: req.json.name, - time: Math.round(new Date().getTime() / 1000), - }); - return result.dataValues.id; - } else { - let result = await global.database.muzic.promiseMode().add({ - name: req.json.name, - author_id: req.json.author_id, - data: - req.json.data === undefined - ? undefined - : Buffer.from(req.json.data, "base64"), - time: Math.round(new Date().getTime() / 1000), - }); - return result.dataValues.id; - } -}; \ No newline at end of file diff --git a/api/v1/methods/edit-item.js b/api/v1/methods/edit-item.js deleted file mode 100644 index 3790da5..0000000 --- a/api/v1/methods/edit-item.js +++ /dev/null @@ -1,90 +0,0 @@ -const ApiError = require("../errorClass"); - -async function isAuthorExists(id) { - if (!id || !Number.isInteger(id)) return false; - return ( - (await global.database.authors.promiseMode().get({ where: { id } })) - .length !== 0 - ); -} - -async function isMuzicExists(id) { - if (!id || !Number.isInteger(id)) return false; - let result = - (await global.database.muzic.promiseMode().get({ where: { id } })) - .length !== 0; - console.log("isMuzicExists", result); - return result; -} - -async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля type - ["author", "muzic"].includes(req.json.type) && // Проверка поля name - (req.json.name === undefined - ? true - : typeof req.json.name === "string" && req.json.name.length >= 2) && // Проверка id - (req.json.type === "author" - ? await isAuthorExists(req.json.id) - : await isMuzicExists(req.json.id)) && // Проверка при type=muzic - (req.json.type === "muzic" - ? // Проверка поля author_id - (req.json.author_id === undefined - ? true - : await isAuthorExists(req.json.author_id)) && // Проверка поля data. (Передаётся либо ничего, либо строка с base64) - (req.json.data === undefined ? true : typeof req.json.data === "string") - : true) - ); -} - -module.exports = async (req, res) => { - if (req.method !== "POST") { - throw new ApiError("METHOD_MUST_BE_POST_JSON", { - request_method: req.method, - }); - } - console.log(await checkSyntaxArgs(req, res)); - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - id: req.json.id === undefined ? null : req.json.id, - type: req.json.type === undefined ? null : req.json.type, - name: req.json.name === undefined ? null : req.json.name, - ...(req.json.type === "muzic" - ? { - author_id: req.json.author_id, - data: req.json.data, - } - : {}), - }, - }); - } - - if (req.json.type === "author") { - await global.database.authors.promiseMode().edit( - { - name: req.json.name, - }, - { - where: { - id: req.json.id, - }, - }, - ); - } else { - await global.database.muzic.promiseMode().edit( - { - name: req.json.name, - author_id: req.json.author_id, - data: !req.json.data ? undefined : Buffer.from(req.json.data, "base64"), - }, - { - where: { - id: req.json.id, - }, - }, - ); - } - return "ok"; -}; \ No newline at end of file diff --git a/api/v1/methods/get-authors.js b/api/v1/methods/get-authors.js deleted file mode 100644 index 1f55801..0000000 --- a/api/v1/methods/get-authors.js +++ /dev/null @@ -1,81 +0,0 @@ -const ApiError = require("../errorClass"); - -async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля offset - (req.query.id === undefined ? true : Number.isInteger(+req.query.offset)) && // Проверка поля offset - (req.query.offset === undefined - ? true - : Number.isInteger(+req.query.offset)) && // Проверка поля count - (req.query.count === undefined - ? true - : Number.isInteger(+req.query.count)) && // Проверка поля min_date - (req.query.min_date === undefined - ? true - : Number.isInteger(+req.query.min_date)) && // Проверка поля max_date - (req.query.max_date === undefined - ? true - : Number.isInteger(+req.query.max_date)) && // Проверка поля q. (Ключевые слова для поиска) - true // (Проверки нет, query всегда строка, необязательный параметр) - ); -} - -module.exports = async (req, res) => { - if (req.method !== "GET") { - throw new ApiError("METHOD_MUST_BE_GET", { - request_method: req.method, - }); - } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - id: req.query?.id === undefined ? null : req.query.id, - q: req.query?.q === undefined ? null : req.query.q, - offset: req.query?.offset === undefined ? null : req.query.offset, - count: req.query?.count === undefined ? null : req.query.count, - min_date: req.query?.min_date === undefined ? null : req.query.min_date, - max_date: req.query?.max_date === undefined ? null : req.query.max_date, - }, - }); - } - - const offset = req.query.offset === undefined ? 0 : +req.query.offset; - const countFromNull = - req.query.count === undefined ? undefined : offset + +req.query.count; - - let result = ( - await global.database.authors.promiseMode().get( - req.query.id === undefined - ? {} - : { - id: +req.query.id, - }, - ) - ).map((i) => ({ - id: i.id, - name: i.name, - date: i.time, - })); - // Если просят выборку по времени - if (req.query.min_date || req.query.max_date) { - const minDate = req.query.min_date === undefined ? 0 : +req.query.min_date; - const maxDate = - req.query.max_date === undefined ? 1e32 : +req.query.max_date; - - result = result.filter((res) => minDate <= res.date && res.date <= maxDate); - } - - if (req.query?.q !== undefined) { - result = result.filter((i) => { - const search = req.query.q.toLowerCase(); - return i.name.toLowerCase().includes(search); - }); - } - - result = result.slice(offset, countFromNull); - return { - count: result.length, - items: result, - }; -}; \ No newline at end of file diff --git a/api/v1/methods/get-muzic.js b/api/v1/methods/get-muzic.js deleted file mode 100644 index 43a9998..0000000 --- a/api/v1/methods/get-muzic.js +++ /dev/null @@ -1,142 +0,0 @@ -const ApiError = require("../errorClass"); - -async function isAuthorExists(authorId) { - if (!authorId) return false; - return ( - ( - await global.database.authors - .promiseMode() - .get({ where: { id: authorId } }) - ).length !== 0 - ); -} - -async function isAuthorsExists(authorsId) { - if (!Array.isArray(authorsId)) return false; - const authors = (await global.database.authors.promiseMode().get()).map( - (i) => i.id, - ); - return authorsId.map((authorId) => authors.includes(authorId)); -} - -async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля author. - (req.query.author === undefined - ? true - : await isAuthorExists(+req.query.author)) && // Проверка поля authors. - (req.query.authors === undefined - ? true - : await isAuthorsExists(req.query.authors.split(",").map((i) => +i))) && // Проверка поля offset - (req.query.offset === undefined - ? true - : Number.isInteger(+req.query.offset)) && // Проверка поля count - (req.query.count === undefined - ? true - : Number.isInteger(+req.query.count)) && // Проверка поля min_date - (req.query.min_date === undefined - ? true - : Number.isInteger(+req.query.min_date)) && // Проверка поля max_date - (req.query.max_date === undefined - ? true - : Number.isInteger(+req.query.max_date)) && // Проверка поля q. (Ключевые слова для поиска) - true && // (Проверки нет, query всегда строка, необязательный параметр) // Проверка поля searchByAuthor (Флаг для 'q'. 0 - отключить) - true && // (Проверки нет, query всегда строка, необязательный параметр) // Проверка поля searchByName (Флаг для 'q'. 0 - отключить) - true // (Проверки нет, query всегда строка, необязательный параметр) - ); -} - -module.exports = async (req, res) => { - if (req.method !== "GET") { - throw new ApiError("METHOD_MUST_BE_GET", { - request_method: req.method, - }); - } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - author: req.query?.author === undefined ? null : req.query.author, - authors: req.query?.authors === undefined ? null : req.query.authors, - q: req.query?.q === undefined ? null : req.query.q, - offset: req.query?.offset === undefined ? null : req.query.offset, - count: req.query?.count === undefined ? null : req.query.count, - searchByAuthor: - req.query?.searchByAuthor === undefined - ? null - : req.query.searchByAuthor, - searchByName: - req.query?.searchByName === undefined ? null : req.query.searchByName, - min_date: req.query?.min_date === undefined ? null : req.query.min_date, - max_date: req.query?.max_date === undefined ? null : req.query.max_date, - }, - }); - } - - // Оптимизация: флаг searchByAuthor выставится на 0, если была запрошена выборка по музыке определённого автора - // Тем самым, мы ускоряем время ответа, поскольку мы не перебираем массив searchedAuthors с целью поиска совпадений - let searchByAuthor = - req.query.author === undefined ? req.query.searchByAuthor : "0"; - - const offset = req.query.offset === undefined ? 0 : +req.query.offset; - const countFromNull = - req.query.count === undefined ? undefined : offset + +req.query.count; - - let result = ( - await global.database.muzic.promiseMode().get( - req.query?.author === undefined - ? {} - : { - where: { - author_id: +req.query.author, - }, - }, - ) - ).map((i) => ({ - id: i.id, - name: i.name, - author_id: i.author_id, - is_data_exists: i.data !== null, - date: i.time, - })); - // Если просят выборку по authors - if (req.query.authors) { - let authors = req.query.authors.split(",").map((author) => +author); - result = result.filter((res) => authors.includes(res.author_id)); - } - // Если просят выборку по времени - if (req.query.min_date || req.query.max_date) { - const minDate = req.query.min_date === undefined ? 0 : +req.query.min_date; - const maxDate = - req.query.max_date === undefined ? 1e32 : +req.query.max_date; - - result = result.filter((res) => minDate <= res.date && res.date <= maxDate); - } - - if (req.query?.q !== undefined) { - let authors = await global.database.authors.promiseMode().get(); - let searchedAuthors = result.map((i) => { - let author_id = i.author_id; - return authors.filter((a) => a.id === author_id)[0]; - }); - result = result.filter((i) => { - const search = req.query.q.toLowerCase(); - return ( - (req.query.searchByName !== "0" && - i.name.toLowerCase().includes(search)) || - !!( - searchByAuthor !== "0" && - searchedAuthors - .filter((a) => a.id === i.author_id)[0] - ?.name.toLowerCase() - .includes(search) - ) - ); - }); - } - result = result.slice(offset, countFromNull); - return { - count: result.length, - items: result, - }; -}; \ No newline at end of file diff --git a/api/v1/methods/remove-item.js b/api/v1/methods/remove-item.js deleted file mode 100644 index b0c288a..0000000 --- a/api/v1/methods/remove-item.js +++ /dev/null @@ -1,72 +0,0 @@ -const ApiError = require("../errorClass"); -const config = require("../../../config-handler"); - -async function isAuthorExists(authorId) { - if (!authorId) return false; - return ( - ( - await global.database.authors - .promiseMode() - .get({ where: { id: authorId } }) - ).length !== 0 - ); -} - -async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля type. - ["author", "muzic"].indexOf(req.json?.type) !== -1 && // Проверка поля id. - (Number.isInteger(req.json.id) && req.json.type === "author" - ? await isAuthorExists(req.json.id) - : true) - ); -} - -module.exports = async (req, res) => { - if (req.json === undefined) { - // console.log(req.headers); - throw new ApiError("METHOD_MUST_BE_POST_JSON", { - request_method: req.method, - "content-type": !req.headers["content-type"] - ? null - : req.headers["content-type"], - }); - } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - type: req.json?.type === undefined ? null : req.json.type, - name: req.json?.name === undefined ? null : req.json.name, - ...(req.json?.type === "muzic" - ? { - author_id: - req.json?.author_id === undefined ? null : req.json?.author_id, - data: req.json?.data === undefined ? null : req.json.data, - } - : {}), - }, - }); - } - if (req.json.type === "author") { - // Удаляем всю музыку этого исполнителя - await global.database.muzic.promiseMode().remove({ - where: { - author_id: req.json.id, - }, - }); - // Удаляем исполнителя - await global.database.authors.promiseMode().remove({ - where: { - id: req.json.id, - }, - }); - } else { - await global.database.muzic.promiseMode().remove({ - where: { - id: req.json.id, - }, - }); - } - return "ok"; -}; \ No newline at end of file diff --git a/api/v1/response-wrapper.js b/api/v1/response-wrapper.js deleted file mode 100644 index c40a381..0000000 --- a/api/v1/response-wrapper.js +++ /dev/null @@ -1,61 +0,0 @@ -const fs = require("fs"); -const config = require("../../config-handler"); - -const unknownError = (err) => { - const stackId = new Date().getTime(); - let errorLoggingFolder = config().error_logs_folder; - errorLoggingFolder = !["/", "\\"].includes(errorLoggingFolder.at(-1)) - ? errorLoggingFolder + "/" - : errorLoggingFolder; - fs.writeFileSync( - `${errorLoggingFolder}error_${stackId}.log`, - `ERROR: -Date: ${new Date()} -Name: ${err.name} -Message: ${err.message} -Stack: -${err.stack}`, - ); - - return { - error: "UNKNOWN_ERROR", - details: { - trace_id: stackId, - }, - }; -}; - -const unknownResponseFormat = "UNKNOWN_RESPONSE_FORMAT"; - -async function handlingError(funct, success, error) { - try { - success(await funct()); - } catch (e) { - // console.log('error', e); - error( - e.name === "ApiError" - ? { - error: e.message, - details: e.details, - } - : unknownError(e), - ); - } -} - -module.exports = async (method, req, res) => { - if (req.query.response_format === "json") { - handlingError( - async () => ({ response: await method(req, res) }), - async (data) => { - res.sendModed(data); - }, - async (errBody) => { - res.errorModeOn(); - res.status(400).sendModed(errBody); - }, - ); - } else { - res.status(400).sendModed(unknownResponseFormat); - } -}; \ No newline at end of file diff --git a/config-handler.js b/config-handler.js deleted file mode 100644 index 464d901..0000000 --- a/config-handler.js +++ /dev/null @@ -1,8 +0,0 @@ -const fs = require("fs"); -module.exports = () => { - const config = JSON.parse( - fs.readFileSync("./config.json", { encoding: "utf-8" }), - ); - // Проверить конфиг на целостность - return config; -}; diff --git a/config.json b/config.json deleted file mode 100644 index 0eaa747..0000000 --- a/config.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "address" : "127.0.0.1", - "port" : 8080, - "ssl" : { - "enabled" : false, - "public" : "", - "private" : "" - }, - "database" : { - "port" : 5433, - "address" : "localhost", - "username" : "postgres", - "password" : "root", - "database" : "kodex-db" - }, - "logger_mode" : true, - "logger_folder" : "./logger", - - "error_logs_folder" : "./errors", - - "authors_blacklist" : [ - "Монеточка", "Monetochka" - ] -} \ No newline at end of file diff --git a/database.js b/database.js deleted file mode 100644 index 448d1fb..0000000 --- a/database.js +++ /dev/null @@ -1,136 +0,0 @@ -const { Sequelize, DataTypes } = require("sequelize"); - -class Table { - constructor(model) { - this.model = model; - } - - promiseMode() { - return new TablePromise(this.model); - } - - get(cb, condition = null) { - this.model - .findAll({ ...(!condition ? {} : condition), raw: true }) - .then((data) => cb(null, data)) - .catch((err) => cb(err, null)); - } - - add(item, cb) { - this.model - .create(item) - .then((i) => cb(null, i)) - .catch((err) => cb(err, null)); - } - - remove(condition, cb) { - this.model - .destroy(condition) - .then((i) => cb(null, i)) - .catch((err) => cb(err, null)); - } - - edit(data, condition, cb) { - this.model - .update(data, condition) - .then((i) => cb(null, i)) - .catch((err) => cb(err, null)); - } -} - -class TablePromise extends Table { - async get(condition) { - return await this.model.findAll({ - ...(!condition ? {} : condition), - raw: true, - }); - } - - async add(item) { - //setTimeout(() => this.model.findAll(), 0); - return await this.model.create(item); - } - - async remove(condition) { - return await this.model.destroy(condition); - } - - async edit(data, condition) { - return await this.model.update(data, condition); - } -} - -class Database { - constructor(address, port, username, password, database) { - this.sequelize = new Sequelize( - `postgres://${username}:${password}@${address}:${port}/${database}`, - ); - this.sequelize.authenticate().then( - () => { - this.authors = new Table( - this.sequelize.define( - "authors", - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false, - }, - name: { - type: DataTypes.TEXT, - allowNull: false, - }, - time: { - type: DataTypes.INTEGER, - allowNull: false, - }, - }, - { - freezeTableName: true, - timestamps: false, - }, - ), - ); - this.muzic = new Table( - this.sequelize.define( - "muzic", - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false, - }, - name: { - type: DataTypes.TEXT, - allowNull: false, - }, - author_id: { - type: DataTypes.INTEGER, - allowNull: false, - }, - data: { - type: DataTypes.BLOB("long"), - }, - time: { - type: DataTypes.INTEGER, - allowNull: false, - }, - }, - { - freezeTableName: true, - timestamps: false, - }, - ), - ); - console.log("Database successful connected!"); - }, - (err) => { - throw err; - }, - ); - } -} - -module.exports = { Database }; diff --git a/db-dump/install-it.sql b/db-dump/install-it.sql deleted file mode 100644 index c033bb6..0000000 Binary files a/db-dump/install-it.sql and /dev/null differ diff --git a/frontend/main.html b/frontend/main.html deleted file mode 100644 index 144d6c4..0000000 --- a/frontend/main.html +++ /dev/null @@ -1,41 +0,0 @@ - - -
-`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `