mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2026-05-09 18:27:56 +08:00
✨ 添加 Theme 功能
This commit is contained in:
@@ -1,3 +1 @@
|
||||
from .post import Post
|
||||
|
||||
__all__ = ["Post"]
|
||||
from .post import Post as Post
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
from functools import reduce
|
||||
from abc import abstractmethod
|
||||
from dataclasses import field, dataclass
|
||||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from nonebot_plugin_saa import MessageFactory, MessageSegmentFactory
|
||||
from nonebot_plugin_saa import Text, MessageFactory, MessageSegmentFactory
|
||||
|
||||
from ..utils import text_to_image
|
||||
from ..plugin_config import plugin_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class BasePost:
|
||||
@abstractmethod
|
||||
async def generate_text_messages(self) -> list[MessageSegmentFactory]:
|
||||
"Generate MessageFactory list from this instance"
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def generate_pic_messages(self) -> list[MessageSegmentFactory]:
|
||||
"Generate MessageFactory list from this instance with `use_pic`"
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class OptionalMixin:
|
||||
# Because of https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses
|
||||
|
||||
override_use_pic: bool | None = None
|
||||
@dataclass(kw_only=True)
|
||||
class AbstractPost(ABC):
|
||||
compress: bool = False
|
||||
extra_msg: list[MessageFactory] = field(default_factory=list)
|
||||
extra_msg: list[MessageFactory] | None = None
|
||||
|
||||
def _use_pic(self):
|
||||
if self.override_use_pic is not None:
|
||||
return self.override_use_pic
|
||||
return plugin_config.bison_use_pic
|
||||
@abstractmethod
|
||||
async def generate(self) -> list[MessageSegmentFactory]:
|
||||
"Generate MessageSegmentFactory list from this instance"
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class AbstractPost(OptionalMixin, BasePost):
|
||||
async def generate_messages(self) -> list[MessageFactory]:
|
||||
if self._use_pic():
|
||||
msg_segments = await self.generate_pic_messages()
|
||||
else:
|
||||
msg_segments = await self.generate_text_messages()
|
||||
if msg_segments:
|
||||
if self.compress:
|
||||
msgs = [reduce(lambda x, y: x.append(y), msg_segments, MessageFactory([]))]
|
||||
else:
|
||||
msgs = [MessageFactory([msg_segment]) for msg_segment in msg_segments]
|
||||
else:
|
||||
msgs = []
|
||||
msgs.extend(self.extra_msg)
|
||||
"really call to generate messages"
|
||||
msg_segments = await self.generate()
|
||||
msg_segments = await self.message_segments_process(msg_segments)
|
||||
msgs = await self.message_process(msg_segments)
|
||||
return msgs
|
||||
|
||||
async def message_segments_process(self, msg_segments: list[MessageSegmentFactory]) -> list[MessageSegmentFactory]:
|
||||
"generate message segments and process them"
|
||||
|
||||
async def convert(msg: MessageSegmentFactory) -> MessageSegmentFactory:
|
||||
if isinstance(msg, Text):
|
||||
return await text_to_image(msg)
|
||||
else:
|
||||
return msg
|
||||
|
||||
if plugin_config.bison_use_pic:
|
||||
return [await convert(msg) for msg in msg_segments]
|
||||
|
||||
return msg_segments
|
||||
|
||||
async def message_process(self, msg_segments: list[MessageSegmentFactory]) -> list[MessageFactory]:
|
||||
"generate messages and process them"
|
||||
if self.compress:
|
||||
msgs = [MessageFactory(msg_segments)]
|
||||
else:
|
||||
msgs = [MessageFactory(msg_segment) for msg_segment in msg_segments]
|
||||
|
||||
if self.extra_msg:
|
||||
msgs.extend(self.extra_msg)
|
||||
|
||||
return msgs
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
from dataclasses import field, dataclass
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.plugin import require
|
||||
from nonebot.adapters.onebot.v11 import MessageSegment
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from .abstract_post import BasePost, AbstractPost
|
||||
|
||||
|
||||
@dataclass
|
||||
class _CustomPost(BasePost):
|
||||
ms_factories: list[MessageSegmentFactory] = field(default_factory=list)
|
||||
css_path: str = "" # 模板文件所用css路径
|
||||
|
||||
async def generate_text_messages(self) -> list[MessageSegmentFactory]:
|
||||
return self.ms_factories
|
||||
|
||||
async def generate_pic_messages(self) -> list[MessageSegmentFactory]:
|
||||
require("nonebot_plugin_htmlrender")
|
||||
from nonebot_plugin_htmlrender import md_to_pic
|
||||
|
||||
pic_bytes = await md_to_pic(md=self._generate_md(), css_path=self.css_path)
|
||||
return [Image(pic_bytes)]
|
||||
|
||||
def _generate_md(self) -> str:
|
||||
md = ""
|
||||
|
||||
for message_segment in self.ms_factories:
|
||||
match message_segment:
|
||||
case Text(data={"text": text}):
|
||||
md += f"{text}<br>"
|
||||
case Image(data={"image": image}):
|
||||
# use onebot v11 to convert image into url
|
||||
ob11_image = MessageSegment.image(image)
|
||||
md += "\n".format(ob11_image.data["file"])
|
||||
case _:
|
||||
logger.warning(f"custom_post不支持处理类型:{type(message_segment)}")
|
||||
continue
|
||||
|
||||
return md
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomPost(_CustomPost, AbstractPost):
|
||||
"""基于 markdown 语法的,自由度较高的推送内容格式
|
||||
|
||||
简介:
|
||||
支持处理text/image两种MessageSegmentFactory,
|
||||
通过将text/image转换成对应的markdown语法以生成markdown文本。
|
||||
理论上text类型中可以直接使用markdown语法,例如`##第一章`。
|
||||
但会导致不启用`override_use_pic`时, 发送不会被渲染的纯文本消息。
|
||||
图片渲染最终由htmlrender执行。
|
||||
|
||||
注意:
|
||||
每一个MessageSegmentFactory元素都会被解释为单独的一行
|
||||
|
||||
可选参数:
|
||||
`override_use_pic`:是否覆盖`bison_use_pic`全局配置
|
||||
`compress`:将所有消息压缩为一条进行发送
|
||||
`extra_msg`:需要附带发送的额外消息
|
||||
|
||||
成员函数:
|
||||
`generate_text_messages()`:负责生成文本消息
|
||||
`generate_pic_messages()`:负责生成图片消息
|
||||
"""
|
||||
|
||||
pass
|
||||
+72
-139
@@ -1,151 +1,84 @@
|
||||
from io import BytesIO
|
||||
from dataclasses import field, dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PIL import Image
|
||||
from nonebot.log import logger
|
||||
import nonebot_plugin_saa as saa
|
||||
from nonebot_plugin_saa import MessageSegmentFactory
|
||||
|
||||
from ..utils import parse_text, http_client
|
||||
from .abstract_post import BasePost, AbstractPost
|
||||
from ..theme import theme_manager
|
||||
from .abstract_post import AbstractPost
|
||||
from ..plugin_config import plugin_config
|
||||
from ..theme.types import ThemeRenderError, ThemeRenderUnsupportError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..platform import Platform
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Post(BasePost):
|
||||
target_type: str
|
||||
text: str
|
||||
class Post(AbstractPost):
|
||||
"""最通用的Post,理论上包含所有常用的数据
|
||||
|
||||
对于更特殊的需要,可以考虑另外实现一个Post
|
||||
"""
|
||||
|
||||
platform: "Platform"
|
||||
"""来源平台"""
|
||||
content: str
|
||||
"""文本内容"""
|
||||
title: str | None = None
|
||||
"""标题"""
|
||||
images: list[str | bytes | Path | BytesIO] | None = None
|
||||
"""图片列表"""
|
||||
timestamp: int | None = None
|
||||
"""发布/获取时间戳"""
|
||||
url: str | None = None
|
||||
target_name: str | None = None
|
||||
pics: list[str | bytes] = field(default_factory=list)
|
||||
"""来源链接"""
|
||||
avatar: str | bytes | Path | BytesIO | None = None
|
||||
"""发布者头像"""
|
||||
nickname: str | None = None
|
||||
"""发布者昵称"""
|
||||
description: str | None = None
|
||||
"""发布者个性签名等"""
|
||||
repost: "Post | None" = None
|
||||
"""转发的Post"""
|
||||
|
||||
_message: list[MessageSegmentFactory] | None = None
|
||||
_pic_message: list[MessageSegmentFactory] | None = None
|
||||
def get_config_theme(self) -> str | None:
|
||||
"""获取用户指定的theme"""
|
||||
return plugin_config.bison_platform_theme.get(self.platform.platform_name)
|
||||
|
||||
async def _pic_url_to_image(self, data: str | bytes) -> Image.Image:
|
||||
pic_buffer = BytesIO()
|
||||
if isinstance(data, str):
|
||||
async with http_client() as client:
|
||||
res = await client.get(data)
|
||||
pic_buffer.write(res.content)
|
||||
def get_priority_themes(self) -> list[str]:
|
||||
"""获取渲染所使用的theme名列表,按照优先级排序"""
|
||||
themes_by_priority: list[str] = []
|
||||
# 最先使用用户指定的theme
|
||||
if user_theme := self.get_config_theme():
|
||||
themes_by_priority.append(user_theme)
|
||||
# 然后使用平台默认的theme
|
||||
if self.platform.default_theme not in themes_by_priority:
|
||||
themes_by_priority.append(self.platform.default_theme)
|
||||
# 最后使用最基础的theme
|
||||
if "basic" not in themes_by_priority:
|
||||
themes_by_priority.append("basic")
|
||||
return themes_by_priority
|
||||
|
||||
async def generate(self) -> list[MessageSegmentFactory]:
|
||||
"""生成消息"""
|
||||
themes = self.get_priority_themes()
|
||||
for theme_name in themes:
|
||||
if theme := theme_manager[theme_name]:
|
||||
try:
|
||||
logger.debug(f"Try to render Post with theme {theme_name}")
|
||||
return await theme.do_render(self)
|
||||
except ThemeRenderUnsupportError as e:
|
||||
logger.warning(
|
||||
f"Theme {theme_name} does not support Post of {self.platform.__class__.__name__}: {e}"
|
||||
)
|
||||
continue
|
||||
except ThemeRenderError as e:
|
||||
logger.exception(f"Theme {theme_name} render error: {e}")
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Theme {theme_name} not found")
|
||||
continue
|
||||
else:
|
||||
pic_buffer.write(data)
|
||||
return Image.open(pic_buffer)
|
||||
|
||||
def _check_image_square(self, size: tuple[int, int]) -> bool:
|
||||
return abs(size[0] - size[1]) / size[0] < 0.05
|
||||
|
||||
async def _pic_merge(self) -> None:
|
||||
if len(self.pics) < 3:
|
||||
return
|
||||
first_image = await self._pic_url_to_image(self.pics[0])
|
||||
if not self._check_image_square(first_image.size):
|
||||
return
|
||||
images: list[Image.Image] = [first_image]
|
||||
# first row
|
||||
for i in range(1, 3):
|
||||
cur_img = await self._pic_url_to_image(self.pics[i])
|
||||
if not self._check_image_square(cur_img.size):
|
||||
return
|
||||
if cur_img.size[1] != images[0].size[1]: # height not equal
|
||||
return
|
||||
images.append(cur_img)
|
||||
_tmp = 0
|
||||
x_coord = [0]
|
||||
for i in range(3):
|
||||
_tmp += images[i].size[0]
|
||||
x_coord.append(_tmp)
|
||||
y_coord = [0, first_image.size[1]]
|
||||
|
||||
async def process_row(row: int) -> bool:
|
||||
if len(self.pics) < (row + 1) * 3:
|
||||
return False
|
||||
row_first_img = await self._pic_url_to_image(self.pics[row * 3])
|
||||
if not self._check_image_square(row_first_img.size):
|
||||
return False
|
||||
if row_first_img.size[0] != images[0].size[0]:
|
||||
return False
|
||||
image_row: list[Image.Image] = [row_first_img]
|
||||
for i in range(row * 3 + 1, row * 3 + 3):
|
||||
cur_img = await self._pic_url_to_image(self.pics[i])
|
||||
if not self._check_image_square(cur_img.size):
|
||||
return False
|
||||
if cur_img.size[1] != row_first_img.size[1]:
|
||||
return False
|
||||
if cur_img.size[0] != images[i % 3].size[0]:
|
||||
return False
|
||||
image_row.append(cur_img)
|
||||
images.extend(image_row)
|
||||
y_coord.append(y_coord[-1] + row_first_img.size[1])
|
||||
return True
|
||||
|
||||
if await process_row(1):
|
||||
matrix = (3, 2)
|
||||
else:
|
||||
matrix = (3, 1)
|
||||
if await process_row(2):
|
||||
matrix = (3, 3)
|
||||
logger.info("trigger merge image")
|
||||
target = Image.new("RGB", (x_coord[-1], y_coord[-1]))
|
||||
for y in range(matrix[1]):
|
||||
for x in range(matrix[0]):
|
||||
target.paste(
|
||||
images[y * matrix[0] + x],
|
||||
(x_coord[x], y_coord[y], x_coord[x + 1], y_coord[y + 1]),
|
||||
)
|
||||
target_io = BytesIO()
|
||||
target.save(target_io, "JPEG")
|
||||
self.pics = self.pics[matrix[0] * matrix[1] :]
|
||||
self.pics.insert(0, target_io.getvalue())
|
||||
|
||||
async def generate_text_messages(self) -> list[MessageSegmentFactory]:
|
||||
if self._message is None:
|
||||
await self._pic_merge()
|
||||
msg_segments: list[MessageSegmentFactory] = []
|
||||
text = ""
|
||||
if self.text:
|
||||
text += "{}".format(self.text if len(self.text) < 500 else self.text[:500] + "...")
|
||||
if text:
|
||||
text += "\n"
|
||||
text += f"来源: {self.target_type}"
|
||||
if self.target_name:
|
||||
text += f" {self.target_name}"
|
||||
if self.url:
|
||||
text += f" \n详情: {self.url}"
|
||||
msg_segments.append(saa.Text(text))
|
||||
for pic in self.pics:
|
||||
msg_segments.append(saa.Image(pic))
|
||||
self._message = msg_segments
|
||||
return self._message
|
||||
|
||||
async def generate_pic_messages(self) -> list[MessageSegmentFactory]:
|
||||
if self._pic_message is None:
|
||||
await self._pic_merge()
|
||||
msg_segments: list[MessageSegmentFactory] = []
|
||||
text = ""
|
||||
if self.text:
|
||||
text += f"{self.text}"
|
||||
text += "\n"
|
||||
text += f"来源: {self.target_type}"
|
||||
if self.target_name:
|
||||
text += f" {self.target_name}"
|
||||
msg_segments.append(await parse_text(text))
|
||||
if not self.target_type == "rss" and self.url:
|
||||
msg_segments.append(saa.Text(self.url))
|
||||
for pic in self.pics:
|
||||
msg_segments.append(saa.Image(pic))
|
||||
self._pic_message = msg_segments
|
||||
return self._pic_message
|
||||
|
||||
def __str__(self):
|
||||
return "type: {}\nfrom: {}\ntext: {}\nurl: {}\npic: {}".format(
|
||||
self.target_type,
|
||||
self.target_name,
|
||||
self.text if len(self.text) < 500 else self.text[:500] + "...",
|
||||
self.url,
|
||||
", ".join("b64img" if isinstance(x, bytes) or x.startswith("base64") else x for x in self.pics),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Post(AbstractPost, _Post):
|
||||
pass
|
||||
raise ThemeRenderError(f"No theme can render Post of {self.platform.__class__.__name__}")
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1"
|
||||
/>
|
||||
<link rel="icon" href="data:;base64,=" />
|
||||
<title>公告</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<div class="standerd-container">
|
||||
{% if bannerImageUrl %}
|
||||
<div class="banner-image-container">
|
||||
<img class="banner-image" src="{{ bannerImageUrl }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="head-title-container">
|
||||
<span class="head-title">{{ announce_title }}</span>
|
||||
</div>
|
||||
<div class="content">{{ content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
引用自 https://ak.hycdn.cn/announce/assets/css/announcement.v_0_1_2.css
|
||||
**/
|
||||
@media screen and (max-device-width: 480px) {
|
||||
body {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body,
|
||||
head {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background-color: #313131;
|
||||
min-height: 100%;
|
||||
background-color: #d0d0cf;
|
||||
}
|
||||
.main {
|
||||
max-width: 980px;
|
||||
font-family: "Microsoft Yahei";
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
font-size: 1rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
.main .container {
|
||||
min-height: 100%;
|
||||
}
|
||||
.main .container .standerd-container {
|
||||
padding: 2.72727273%;
|
||||
width: 94.54545455%;
|
||||
margin: auto;
|
||||
}
|
||||
.main .container .standerd-container .banner-image-container {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.main .container .standerd-container .banner-image-container .banner-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.main .container .standerd-container .head-title-container {
|
||||
margin: 0;
|
||||
background-image: url(
|
||||
https://ak.hycdn.cn/announce/assets/images/announcement/header.jpg);
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.main .container .standerd-container .head-title-container::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-top: 6.02564103%;
|
||||
}
|
||||
.main .container .standerd-container .head-title-container .head-title {
|
||||
padding-left: 0.25rem;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.main .container .standerd-container .content {
|
||||
line-height: 0.8rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.main .container .standerd-container .content h4 {
|
||||
font-size: 110%;
|
||||
margin-block-start: 0.5rem;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
.main .container .standerd-container .content p {
|
||||
margin-block-start: 0.25rem;
|
||||
margin-block-end: 0.25rem;
|
||||
min-height: 0.8rem;
|
||||
}
|
||||
.main .container .standerd-container .content img {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
.main .container .banner-image-container.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main .container .banner-image-container.cover .cover-jumper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.main .container .banner-image-container.cover .banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
@charset "utf-8";
|
||||
|
||||
/**
|
||||
* markdown.css
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Lesser General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see http://gnu.org/licenses/lgpl.txt.
|
||||
*
|
||||
* @project Weblog and Open Source Projects of Florian Wolters
|
||||
* @version GIT: $Id$
|
||||
* @package xhtml-css
|
||||
* @author Florian Wolters <florian.wolters.85@googlemail.com>
|
||||
* @copyright 2012 Florian Wolters
|
||||
* @cssdoc version 1.0-pre
|
||||
* @license http://gnu.org/licenses/lgpl.txt GNU Lesser General Public License
|
||||
* @link http://github.com/FlorianWolters/jekyll-bootstrap-theme
|
||||
* @media all
|
||||
* @valid true
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: Helvetica, Arial, Freesans, clean, sans-serif;
|
||||
padding: 1em;
|
||||
margin: auto;
|
||||
max-width: 42em;
|
||||
background: #fefefe;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #000000;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
color: #000000;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
color: #777777;
|
||||
background-color: inherit;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0.2em;
|
||||
border: 0;
|
||||
color: #CCCCCC;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
||||
p, blockquote, ul, ol, dl, li, table, pre {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
border-radius: 3px;
|
||||
background-color: #F8F8F8;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
border: 1px solid #EAEAEA;
|
||||
margin: 0 2px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: 1px solid #CCCCCC;
|
||||
line-height: 1.25em;
|
||||
overflow: auto;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: #4183C4;
|
||||
background-color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
Reference in New Issue
Block a user