Compare commits

..

6 Commits

Author SHA1 Message Date
mk 645ab9b995 mk_branch
2 months ago
mk 4fc80d8a06 注释
2 months ago
mk 03e47de49f 删除
2 months ago
mk 3873336204 注释
2 months ago
mk 927b3cc2af 11
2 months ago
mk 8991e19bb0 1
2 months ago

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.13 (DjangoBlog-master)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (accounts)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (DjangoBlog-master)" project-jdk-type="Python SDK" />
</project>

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/accounts.iml" filepath="$PROJECT_DIR$/.idea/accounts.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog-master.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog-master.iml" />
</modules>
</component>
</project>

@ -9,16 +9,15 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
#sjt 后台创建用户表单:处理密码设置和验证
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # sjt 密码字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # sjt 确认密码字段
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser # sjt 关联BlogUser模型
fields = ('email',) # sjt 表单包含的字段
model = BlogUser
fields = ('email',)
def clean_password2(self):
#sjt 验证两次输入密码一致性
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
@ -26,31 +25,29 @@ class BlogUserCreationForm(forms.ModelForm):
return password2
def save(self, commit=True):
#sjt 保存用户:加密密码并标记来源为后台
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"]) # sjt 加密密码
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite' # sjt 标记来源为后台添加
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
#sjt 后台修改用户表单:配置修改用户的字段
class Meta:
model = BlogUser # sjt 关联BlogUser模型
fields = '__all__' # sjt 包含所有字段
field_classes = {'username': UsernameField} # sjt 用户名字段类
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
#sjt Admin后台用户管理配置定义显示和操作方式
form = BlogUserChangeForm # sjt 修改用户表单
add_form = BlogUserCreationForm # sjt 创建用户表单
list_display = ( # sjt 列表页显示的字段
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
@ -58,6 +55,6 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username') # sjt 列表页可点击的链接字段
ordering = ('-id',) # sjt 按ID倒序排列
search_fields = ('username', 'nickname', 'email') # sjt 搜索字段
list_display_links = ('id', 'username')
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -9,48 +9,39 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
#sjt 登录表单:定义登录表单字段样式
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# sjt 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# sjt 设置密码输入框样式
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
#sjt 注册表单:验证用户名、邮箱、密码合法性
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# sjt 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# sjt 设置邮箱输入框样式
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# sjt 设置密码输入框样式
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# sjt 设置确认密码输入框样式
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
#sjt 验证邮箱唯一性:已存在则抛出异常
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model() # sjt 关联用户模型
fields = ("username", "email") # sjt 表单包含的字段
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
#sjt 密码找回表单:验证邮箱、验证码、新密码合法性
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -59,7 +50,7 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("New password")
}
),
) # sjt 新密码字段
)
new_password2 = forms.CharField(
label="确认密码",
@ -69,7 +60,7 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("Confirm password")
}
),
) # sjt 确认密码字段
)
email = forms.EmailField(
label='邮箱',
@ -79,7 +70,7 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("Email")
}
),
) # sjt 邮箱字段
)
code = forms.CharField(
label=_('Code'),
@ -89,28 +80,27 @@ class ForgetPasswordForm(forms.Form):
'placeholder': _("Code")
}
),
) # sjt 验证码字段
)
def clean_new_password2(self):
#sjt 验证两次输入密码一致性,并检查密码强度
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2) # sjt 调用Django密码验证器
password_validation.validate_password(password2)
return password2
def clean_email(self):
#sjt 验证邮箱是否已注册
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
#sjt 验证验证码有效性
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
@ -122,7 +112,6 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
#sjt 发送验证码表单:验证邮箱格式
email = forms.EmailField(
label=_('Email'),
) # sjt 邮箱字段(用于发送验证码)
)

@ -9,31 +9,27 @@ from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
#sjt 扩展Django内置用户模型增加博客系统所需的用户属性
nickname = models.CharField(_('nick name'), max_length=100, blank=True) # sjt 用户昵称
creation_time = models.DateTimeField(_('creation time'), default=now) # sjt 账号创建时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # sjt 信息最后修改时间
source = models.CharField(_('create source'), max_length=100, blank=True) # sjt 账号创建来源(如注册、后台添加)
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
#sjt 获取用户详情页的相对URL
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
#sjt 对象字符串表示,返回用户邮箱
return self.email
def get_full_url(self):
#sjt 获取用户详情页的完整URL包含域名
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id'] # sjt 按ID倒序排列
verbose_name = _('user') # sjt 模型显示名称
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id' # sjt 最新记录按ID判断
get_latest_by = 'id'

@ -13,7 +13,6 @@ from . import utils
class AccountTest(TestCase):
def setUp(self):
#sjt 测试初始化:创建测试客户端和测试用户
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
@ -21,19 +20,16 @@ class AccountTest(TestCase):
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # sjt 测试用新密码
self.new_test = "xxx123--="
def test_validate_account(self):
#sjt 测试用户账号验证:管理员登录、文章创建等功能
site = get_current_site().domain
# sjt 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# sjt 测试登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
@ -41,14 +37,12 @@ class AccountTest(TestCase):
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
# sjt 测试创建分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# sjt 测试创建文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@ -58,30 +52,24 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# sjt 测试访问文章管理页
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#sjt 测试注册流程:注册->邮箱验证->登录->功能验证
# sjt 验证初始状态无此用户
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# sjt 提交注册表单
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# sjt 验证用户创建成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# sjt 验证邮箱并激活用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
@ -90,32 +78,59 @@ class AccountTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# sjt 测试登录及权限
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
# ... 后续测试创建分类、文章等功能
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
#sjt 测试验证码验证功能
to_email = "admin@admin.com"
code = generate_code() # sjt 生成验证码
utils.set_code(to_email, code) # sjt 缓存验证码
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
# sjt 验证正确验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# sjt 验证错误邮箱的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#sjt 测试成功发送密码找回验证码
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
@ -125,7 +140,6 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#sjt 测试发送密码找回验证码失败情况(无效邮箱)
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
@ -139,7 +153,6 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
#sjt 测试成功找回密码流程
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
@ -154,15 +167,14 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.status_code, 302)
# sjt 验证密码是否修改成功
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first()
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
#sjt 测试使用未注册邮箱找回密码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@ -176,19 +188,20 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
#sjt 测试使用错误验证码找回密码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # sjt 错误验证码
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200)

@ -4,25 +4,25 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts" # sjt 应用命名空间
app_name = "accounts"
urlpatterns = [re_path(r'^login/$', # sjt 登录页面路由
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$', # sjt 注册页面路由
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$', # sjt 注销功能路由
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html', # sjt 注册/验证结果页路由
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$', # sjt 密码找回页面路由
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$', # sjt 发送密码找回验证码路由
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
]

@ -0,0 +1,26 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -7,16 +7,16 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5) # sjt 验证码有效期5分钟
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
'''sjt 发送密码重置验证码邮件
"""发送重设密码验证码
Args:
to_mail: 邮箱
to_mail: 邮箱
subject: 邮件主题
code: 验证码
'''
"""
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
@ -24,23 +24,26 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
def verify(email: str, code: str) -> typing.Optional[str]:
'''sjt 验证验证码有效性
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误返回错误信息字符串否则返回None
'''
cache_code = get_code(email) # sjt 从缓存获取验证码
if cache_code != code: # sjt 验证码不匹配
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
#sjt 将验证码存入缓存有效期5分钟
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
#sjt 从缓存获取验证码
return cache.get(email)
"""获取code"""
return cache.get(email)

@ -32,34 +32,28 @@ logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
#sjt 用户注册视图:处理注册表单提交,发送邮箱验证链接
form_class = RegisterForm # sjt 注册表单类
template_name = 'account/registration_form.html' # sjt 注册页面模板
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#sjt 处理请求分发添加CSRF保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#sjt 表单验证通过后:创建未激活用户,生成验证链接并发送邮件
if form.is_valid():
user = form.save(False) # sjt 不立即保存用户
user.is_active = False # sjt 初始为未激活状态(需邮箱验证)
user.source = 'Register' # sjt 标记来源为注册
user.save(True) # sjt 保存用户
site = get_current_site().domain # sjt 获取当前站点域名
# sjt 生成双重加密签名(防止链接篡改)
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG: # sjt 开发环境下使用本地域名
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result') # sjt 获取结果页路由
# sjt 构建验证链接
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# sjt 构建验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -70,7 +64,6 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# sjt 发送验证邮件
send_email(
emailto=[
user.email,
@ -78,7 +71,6 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
# sjt 重定向到注册结果页
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
@ -89,66 +81,60 @@ class RegisterView(FormView):
class LogoutView(RedirectView):
#sjt 用户注销视图:处理用户注销并跳转登录页
url = '/login/' # sjt 注销后跳转的URL
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#sjt 处理请求分发,禁止缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
#sjt 处理GET请求执行注销操作并清除缓存
logout(request) # sjt 注销用户
delete_sidebar_cache() # sjt 删除侧边栏缓存
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
#sjt 用户登录视图:支持用户名/邮箱登录,处理登录状态
form_class = LoginForm # sjt 登录表单类
template_name = 'account/login.html' # sjt 登录页面模板
success_url = '/' # sjt 登录成功默认跳转页
redirect_field_name = REDIRECT_FIELD_NAME # sjt 重定向字段名
login_ttl = 2626560 # sjt 记住登录状态的有效期(一个月,单位秒)
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#sjt 处理请求分发保护敏感参数、CSRF保护、禁止缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#sjt 构建上下文数据获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(** kwargs)
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
#sjt 表单验证通过后:验证用户凭据,处理登录状态(含"记住我"功能)
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid(): # sjt 验证用户凭据
delete_sidebar_cache() # sjt 删除侧边栏缓存
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user()) # sjt 登录用户
if self.request.POST.get("remember"): # sjt 如果勾选"记住我"
self.request.session.set_expiry(self.login_ttl) # sjt 设置会话有效期
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
#sjt 获取登录成功后的跳转URL验证安全性
redirect_to = self.request.POST.get(self.redirect_field_name)
# sjt 验证跳转URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
@ -157,33 +143,31 @@ class LoginView(FormView):
def account_result(request):
#sjt 注册/验证结果页:处理邮箱验证逻辑,激活用户账号
type = request.GET.get('type') # sjt 获取操作类型(注册/验证)
id = request.GET.get('id') # sjt 获取用户ID
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id) # sjt 获取用户对象
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active: # sjt 如果用户已激活,直接跳转首页
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register': # sjt 注册成功结果页
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else: # sjt 邮箱验证结果页
# sjt 验证签名是否正确
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign: # sjt 签名错误返回403
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True # sjt 激活用户
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', { # sjt 渲染结果页
return render(request, 'account/result.html', {
'title': title,
'content': content
})
@ -192,34 +176,29 @@ def account_result(request):
class ForgetPasswordView(FormView):
#sjt 密码找回视图:验证验证码后重置密码
form_class = ForgetPasswordForm # sjt 密码找回表单
template_name = 'account/forget_password.html' # sjt 密码找回页面模板
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
#sjt 表单验证通过后:更新用户密码并跳转登录页
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# sjt 加密并更新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/') # sjt 跳转登录页
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
#sjt 发送密码找回验证码视图:处理发送验证码请求
def post(self, request: HttpRequest):
#sjt 处理POST请求验证邮箱并发送验证码
form = ForgetPasswordCodeForm(request.POST) # sjt 验证邮箱表单
if not form.is_valid(): # sjt 邮箱验证失败
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"] # sjt 获取目标邮箱
to_email = form.cleaned_data["email"]
code = generate_code() # sjt 生成验证码
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
utils.set_code(to_email, code) # sjt 缓存验证码有效期5分钟
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok") # sjt 发送成功返回"ok"
return HttpResponse("ok")

@ -13,26 +13,32 @@ class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
# mk: 指定表单关联的模型和字段
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
# mk: 批量发布文章操作,将选中的文章状态更新为已发布('p')
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
# mk: 批量将文章设为草稿状态,将选中的文章状态更新为草稿('d')
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
# mk: 批量关闭文章评论功能,将选中的文章评论状态更新为关闭('c')
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
# mk: 批量开启文章评论功能,将选中的文章评论状态更新为开启('o')
queryset.update(comment_status='o')
# mk: 为自定义管理操作设置显示名称
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
@ -40,9 +46,13 @@ open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
# mk: 设置文章管理界面每页显示20条记录
list_per_page = 20
# mk: 设置搜索字段,支持在文章正文和标题中搜索
search_fields = ('body', 'title')
# mk: 指定使用的表单类
form = ArticleForm
# mk: 设置在管理界面列表中显示的字段
list_display = (
'id',
'title',
@ -53,36 +63,49 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
# mk: 设置可以作为链接点击的字段
list_display_links = ('id', 'title')
# mk: 设置过滤器,可以在右侧边栏按状态、类型、分类筛选
list_filter = ('status', 'type', 'category')
# mk: 设置日期层级结构,用于按日期筛选文章
date_hierarchy = 'creation_time'
# mk: 设置多对多字段的横向筛选器
filter_horizontal = ('tags',)
# mk: 排除某些字段在表单中显示
exclude = ('creation_time', 'last_modify_time')
# mk: 启用"在站点上查看"功能
view_on_site = True
# mk: 注册自定义的批量操作
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# mk: 设置使用原始ID字段的外键字段提升性能
raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
# mk: 创建指向分类编辑页面的链接
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# mk: 设置分类链接字段的显示名称
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
# mk: 自定义表单,限制作者字段只能选择超级用户
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
# mk: 保存模型实例,调用父类的保存方法
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
# mk: 返回在站点上查看文章的URL
if obj:
url = obj.get_full_url()
return url
@ -93,22 +116,29 @@ class ArticlelAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin):
# mk: 标签管理界面排除某些字段
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
# mk: 设置分类管理界面显示的字段
list_display = ('name', 'parent_category', 'index')
# mk: 分类管理界面排除某些字段
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
# mk: 链接管理界面排除某些字段
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
# mk: 设置侧边栏管理界面显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# mk: 侧边栏管理界面排除某些字段
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
# mk: 博客设置管理界面使用默认配置
pass

@ -0,0 +1,5 @@
from django.apps import AppConfig # mk导入Django的应用配置类#
class BlogConfig(AppConfig): # mk定义博客应用的配置类继承自AppConfig#
name = 'blog' # mk指定应用的名称为'blog'#

@ -0,0 +1,77 @@
import logging # mk导入日志记录模块#
from django.utils import timezone # mk导入Django的时间工具模块#
from djangoblog.utils import cache, get_blog_setting # mk从项目工具模块导入缓存和获取博客设置的函数#
from .models import Category, Article # mk从当前应用的模型中导入分类和文章模型#
logger = logging.getLogger(__name__) # mk创建当前模块的日志记录器#
def seo_processor(requests): # mkSEO处理器函数用于获取网站SEO相关配置信息并缓存#
"""
SEO处理器函数用于获取网站SEO相关配置信息并缓存
该函数从缓存中获取SEO配置信息如果缓存不存在则从数据库获取并设置缓存
主要包含网站基本信息SEO配置导航分类列表页面列表等数据
Args:
requests: HTTP请求对象用于获取请求协议和主机信息
Returns:
dict: 包含SEO配置信息的字典具体包含
- SITE_NAME: 网站名称
- SHOW_GOOGLE_ADSENSE: 是否显示Google Adsense
- GOOGLE_ADSENSE_CODES: Google Adsense代码
- SITE_SEO_DESCRIPTION: 网站SEO描述
- SITE_DESCRIPTION: 网站描述
- SITE_KEYWORDS: 网站关键词
- SITE_BASE_URL: 网站基础URL
- ARTICLE_SUB_LENGTH: 文章摘要长度
- nav_category_list: 导航分类列表
- nav_pages: 导航页面列表
- OPEN_SITE_COMMENT: 是否开启网站评论
- BEIAN_CODE: 备案号
- ANALYTICS_CODE: 统计代码
- BEIAN_CODE_GONGAN: 公安备案号
- SHOW_GONGAN_CODE: 是否显示公安备案号
- CURRENT_YEAR: 当前年份
- GLOBAL_HEADER: 全局头部代码
- GLOBAL_FOOTER: 全局底部代码
- COMMENT_NEED_REVIEW: 评论是否需要审核
"""
key = 'seo_processor' # mk设置缓存键名#
value = cache.get(key) # mk尝试从缓存中获取SEO数据#
# mk检查缓存是否存在如果存在则直接返回缓存数据#
if value:
return value
else:
logger.info('set processor cache.') # mk记录设置缓存的日志信息#
# mk缓存不存在从数据库获取配置信息#
setting = get_blog_setting()
value = { # mk构建包含网站SEO配置和导航数据的字典对象#
'SITE_NAME': setting.site_name, # mk网站名称显示在网页标题等位置#
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # mk是否显示Google Adsense广告的开关#
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # mkGoogle Adsense广告代码内容#
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # mk网站SEO描述信息用于meta标签#
'SITE_DESCRIPTION': setting.site_description, # mk网站描述信息#
'SITE_KEYWORDS': setting.site_keywords, # mk网站关键词用于SEO优化#
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # mk网站基础URL地址由请求协议和主机名组成#
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # mk文章摘要显示长度设置#
'nav_category_list': Category.objects.all(), # mk获取所有文章分类用于导航菜单显示#
'nav_pages': Article.objects.filter( # mk获取所有已发布的页面文章用于导航菜单显示#
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, # mk网站评论功能开关设置#
'BEIAN_CODE': setting.beian_code, # mk网站备案号信息#
'ANALYTICS_CODE': setting.analytics_code, # mk网站统计分析代码#
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # mk公安备案号信息#
"SHOW_GONGAN_CODE": setting.show_gongan_code, # mk是否显示公安备案信息的开关#
"CURRENT_YEAR": timezone.now().year, # mk当前年份用于页面底部显示版权年份#
"GLOBAL_HEADER": setting.global_header, # mk全局页头HTML代码#
"GLOBAL_FOOTER": setting.global_footer, # mk全局页脚HTML代码#
"COMMENT_NEED_REVIEW": setting.comment_need_review, # mk评论是否需要审核的设置#
}
# mk将获取到的数据缓存10小时减少数据库查询压力#
cache.set(key, value, 60 * 60 * 10)
return value # mk返回构建的SEO和导航数据#

@ -7,9 +7,11 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# mk:检查是否启用了Elasticsearch配置
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# mk:创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -19,6 +21,7 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
# mk:尝试获取geoip管道如果不存在则创建
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''{
@ -34,6 +37,11 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
"""
mk:
地理位置信息文档类
用于存储IP地址对应的地理位置信息
"""
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
@ -41,21 +49,41 @@ class GeoIp(InnerDoc):
class UserAgentBrowser(InnerDoc):
"""
mk:
用户代理浏览器信息类
存储浏览器的家族和版本信息
"""
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
"""
mk:
用户代理操作系统信息类
继承自UserAgentBrowser存储操作系统的家族和版本信息
"""
pass
class UserAgentDevice(InnerDoc):
"""
mk:
用户代理设备信息类
存储设备的家族品牌和型号信息
"""
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
"""
mk:
用户代理完整信息类
包含浏览器操作系统设备等完整用户代理信息
"""
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
@ -64,6 +92,11 @@ class UserAgent(InnerDoc):
class ElapsedTimeDocument(Document):
"""
mk:
性能监控文档类
用于记录页面访问性能数据包括URL响应时间访问时间等信息
"""
url = Keyword()
time_taken = Long()
log_datetime = Date()
@ -83,8 +116,19 @@ class ElapsedTimeDocument(Document):
class ElaspedTimeDocumentManager:
"""
mk:
性能监控文档管理类
提供性能监控数据的索引创建删除和保存功能
"""
@staticmethod
def build_index():
"""
mk:
构建性能监控索引
检查索引是否存在如果不存在则初始化索引
"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -93,12 +137,28 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
"""
mk:
删除性能监控索引
删除名为'performance'的索引忽略400和404错误
"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
"""
mk:
创建并保存性能监控记录
Args:
url (str): 访问的URL地址
time_taken (int): 请求耗时毫秒
log_datetime (datetime): 日志记录时间
useragent (object): 用户代理对象包含浏览器系统设备信息
ip (str): 访问者IP地址
"""
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
@ -131,6 +191,11 @@ class ElaspedTimeDocumentManager:
class ArticleDocument(Document):
"""
mk:
文章文档类
用于Elasticsearch中的文章搜索索引包含文章的完整信息
"""
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
author = Object(properties={
@ -165,19 +230,49 @@ class ArticleDocument(Document):
class ArticleDocumentManager():
"""
mk:
文章文档管理类
提供文章索引的创建删除重建和更新功能
"""
def __init__(self):
"""
mk:
初始化文章文档管理器
自动创建索引
"""
self.create_index()
def create_index(self):
"""
mk:
创建文章索引
初始化ArticleDocument索引结构
"""
ArticleDocument.init()
def delete_index(self):
"""
mk:
删除文章索引
删除名为'blog'的索引忽略400和404错误
"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
"""
mk:
将文章模型对象转换为文档对象
Args:
articles (list): Article模型对象列表
Returns:
list: 转换后的ArticleDocument文档对象列表
"""
return [
ArticleDocument(
meta={
@ -202,6 +297,13 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
"""
mk:
重建文章索引
Args:
articles (list, optional): 指定要重建索引的文章列表如果为None则重建所有文章
"""
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
@ -209,5 +311,12 @@ class ArticleDocumentManager():
doc.save()
def update_docs(self, docs):
"""
mk:
批量更新文档
Args:
docs (list): ArticleDocument文档对象列表
"""
for doc in docs:
doc.save()

@ -0,0 +1,39 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
"""
mk:
博客搜索表单类
继承自haystack的SearchForm添加了自定义的查询字段和搜索逻辑
"""
querydata = forms.CharField(required=True)
def search(self):
"""
mk:
执行搜索操作
首先调用父类的搜索方法获取基础搜索结果然后验证表单数据的有效性
如果表单数据有效且查询数据存在则记录查询日志
Returns:
搜索结果数据集
"""
# mk:调用父类的搜索方法获取基础搜索结果
datas = super(BlogSearchForm, self).search()
# mk:验证表单数据,如果无效则返回无查询结果
if not self.is_valid():
return self.no_query_found()
# mk:如果查询数据存在,记录查询日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -4,15 +4,37 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT
ELASTICSEARCH_ENABLED
# TODO 参数化
# mk:TODO 参数化
class Command(BaseCommand):
"""
mk:
Django管理命令类用于构建搜索索引
该命令负责初始化和重建Elasticsearch索引包括_elapsed_time和article两种文档类型
"""
help = 'build search index'
def handle(self, *args, **options):
"""
mk:
处理命令执行逻辑
Args:
*args: 位置参数
**options: 命令行选项参数
Returns:
None
"""
# mk:检查Elasticsearch是否启用
if ELASTICSEARCH_ENABLED:
# mk:构建_elapsed_time文档的索引
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
# mk:重新构建article文档的索引
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -0,0 +1,36 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# mk:TODO 参数化
class Command(BaseCommand):
"""
mk:
Django管理命令类用于构建搜索词列表
该命令从Tag和Category模型中提取名称数据合并去重后输出
"""
help = 'build search words'
def handle(self, *args, **options):
"""
mk:
处理命令的主要逻辑
从Tag和Category模型中获取所有名称合并为一个去重集合
然后将每个名称作为独立行打印输出
Args:
*args: 位置参数
**options: 命令行选项参数
Returns:
None
"""
#mk: 从Tag和Category模型中提取所有名称并合并去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
#mk: 将去重后的名称集合按行打印输出
print('\n'.join(datas))

@ -0,0 +1,32 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand):
"""
mk:
Django管理命令类用于清除整个缓存
该类继承自Django的BaseCommand提供了一个自定义的管理命令
可以通过命令行调用来清除应用的所有缓存数据
"""
help = 'clear the whole cache'
def handle(self, *args, **options):
"""
mk:
处理管理命令的主要逻辑
参数:
*args: 位置参数元组
**options: 关键字参数字典
返回值:
None
"""
# mk:清除所有缓存数据
cache.clear()
# mk:输出成功信息到标准输出
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -6,22 +6,47 @@ from blog.models import Article, Tag, Category
class Command(BaseCommand):
"""
mk:
Django管理命令类用于创建测试数据
该命令会创建测试用户分类标签和文章数据用于开发和测试环境
继承自Django的BaseCommand基类
"""
help = 'create test datas'
def handle(self, *args, **options):
"""
mk:
处理命令执行的主要逻辑
参数:
*args: 位置参数元组
**options: 命令行选项字典
返回值:
None
"""
# mk:创建或获取测试用户
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# mk:创建父级分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# mk:创建子级分类
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
# mk:创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# mk:批量创建20篇文章及其对应标签
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
@ -35,6 +60,8 @@ class Command(BaseCommand):
article.tags.add(basetag)
article.save()
#mk: 清除缓存并输出成功信息
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -8,9 +8,26 @@ site = get_current_site().domain
class Command(BaseCommand):
"""
mk:
Django管理命令类用于向百度搜索引擎推送网站URL
该命令支持推送文章标签分类等不同类型的页面URL到百度搜索引擎
以提高网站内容的收录效率
Attributes:
help (str): 命令帮助信息
"""
help = 'notify baidu url'
def add_arguments(self, parser):
"""
mk:
添加命令行参数
:param parser: 参数解析器对象用于定义命令行参数
:return: None
"""
parser.add_argument(
'data_type',
type=str,
@ -22,21 +39,39 @@ class Command(BaseCommand):
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
"""
mk:
根据相对路径生成完整的HTTPS URL
:param path: 相对路径
:return: 完整的HTTPS URL字符串
"""
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
"""
mk:
命令处理函数根据指定的数据类型收集URL并推送到百度
:param args: 位置参数
:param options: 命令行选项字典包含data_type键
:return: None
"""
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = []
# mk:收集文章URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
# mk:收集标签URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
# mk:收集分类URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
@ -48,3 +83,4 @@ class Command(BaseCommand):
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -8,9 +8,26 @@ from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
"""
mk:
Django管理命令类用于同步用户头像
该命令会遍历所有OAuth用户检查并更新他们的头像URL
确保头像图片可以正常访问如果无法访问则尝试重新获取或使用默认头像
"""
help = 'sync user avatar'
def test_picture(self, url):
"""
mk:
测试图片URL是否可以正常访问
Args:
url (str): 要测试的图片URL地址
Returns:
bool: 如果URL可以正常访问返回True否则返回False
"""
try:
if requests.get(url, timeout=2).status_code == 200:
return True
@ -18,6 +35,17 @@ class Command(BaseCommand):
pass
def handle(self, *args, **options):
"""
mk:
命令处理函数执行用户头像同步逻辑
Args:
*args: 位置参数
**options: 命令行选项参数
Returns:
None
"""
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
@ -25,19 +53,25 @@ class Command(BaseCommand):
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
# mk:如果头像URL以静态URL开头说明可能是本地资源
if url.startswith(static_url):
#mk: 测试图片是否可以正常访问
if self.test_picture(url):
continue
else:
# mk:如果无法访问且用户有元数据则通过OAuth管理器重新获取头像
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
# mk:否则使用默认头像
url = static('blog/img/avatar.png')
else:
# mk:对于非本地资源,直接保存用户头像
url = save_user_avatar(url)
else:
# mk:如果没有头像URL使用默认头像
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
@ -45,3 +79,4 @@ class Command(BaseCommand):
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -10,11 +10,34 @@ logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
"""
mk:
在线用户中间件类用于记录页面加载时间和用户访问信息
该中间件会在每个请求处理前后记录时间计算页面渲染耗时
并将相关信息存储到Elasticsearch中如果启用的话
Args:
get_response: Django中间件的get_response回调函数
"""
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
"""
mk:
中间件调用方法处理请求并记录页面加载时间
记录请求开始时间处理请求计算耗时并将相关信息存储到Elasticsearch
同时替换响应内容中的加载时间占位符
Args:
request: Django HttpRequest对象包含当前请求信息
Returns:
HttpResponse: 处理后的HTTP响应对象
"""
''' page render time '''
start_time = time.time()
response = self.get_response(request)
@ -23,20 +46,24 @@ class OnlineMiddleware(object):
user_agent = parse(http_user_agent)
if not response.streaming:
try:
# mk:计算页面渲染耗时
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
# mk:将耗时转换为毫秒并四舍五入到小数点后两位
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
# mk:创建Elasticsearch文档记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# mk:替换响应内容中的加载时间占位符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response
return response

@ -1,4 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
#mk: Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
@ -8,14 +8,24 @@ import mdeditor.fields
class Migration(migrations.Migration):
"""
mk:
Django 数据库迁移类用于初始化博客应用所需的数据库表结构
该迁移文件定义了博客系统所需的核心数据模型包括网站配置友情链接侧边栏标签分类和文章等
"""
# mk:标记此迁移为初始迁移
initial = True
# mk:定义依赖关系,确保在用户模型之后执行
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# mk:定义具体的数据库操作
operations = [
# mk:创建网站配置模型 BlogSettings
migrations.CreateModel(
name='BlogSettings',
fields=[
@ -41,6 +51,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': '网站配置',
},
),
# mk:创建友情链接模型 Links
migrations.CreateModel(
name='Links',
fields=[
@ -59,6 +71,8 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
# mk:创建侧边栏模型 SideBar
migrations.CreateModel(
name='SideBar',
fields=[
@ -76,6 +90,8 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
# mk:创建标签模型 Tag
migrations.CreateModel(
name='Tag',
fields=[
@ -91,6 +107,8 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
# mk:创建分类模型 Category
migrations.CreateModel(
name='Category',
fields=[
@ -108,6 +126,8 @@ class Migration(migrations.Migration):
'ordering': ['-index'],
},
),
# mk:创建文章模型 Article
migrations.CreateModel(
name='Article',
fields=[
@ -135,3 +155,4 @@ class Migration(migrations.Migration):
},
),
]

@ -1,23 +1,34 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
#mk: Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
"""
mk:
Django数据库迁移类用于向BlogSettings模型添加全局头部和尾部字段
该迁移依赖于blog应用的0001_initial初始迁移包含了两个操作
1. 向BlogSettings模型添加global_footer字段
2. 向BlogSettings模型添加global_header字段
"""
dependencies = [
('blog', '0001_initial'),
]
operations = [
# mk:添加全局尾部字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# mk:添加全局头部字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,27 @@
# mk:Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
"""
mk:
数据库迁移类用于向BlogSettings模型添加评论审核功能
该迁移依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
主要操作是为BlogSettings模型添加一个布尔类型的字段用于控制
评论是否需要管理员审核后才能显示
"""
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
# mk:添加评论审核控制字段到BlogSettings模型
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,45 @@
# mk:Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
""""
mk:
数据库迁移类用于重命名博客设置模型中的字段名
该迁移将以下字段进行重命名
- analyticscode -> analytics_code
- beiancode -> beian_code
- sitename -> site_name
继承自 migrations.Migration 遵循Django的数据库迁移机制
"""
# mk:定义迁移依赖关系确保在执行当前迁移前blog应用的0003_blogsettings_comment_need_review迁移已经完成
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# mk:定义具体的迁移操作列表
operations = [
# mk:将BlogSettings模型中的analyticscode字段重命名为analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
# mk:将BlogSettings模型中的beiancode字段重命名为beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
#mk:将BlogSettings模型中的sitename字段重命名为site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -0,0 +1,651 @@
# mk:Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
# mk:Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
"""
mk:
Django 数据库迁移类用于更新博客应用中的模型字段和选项
此迁移主要完成以下操作
- 更新多个模型的元数据如排序方式显示名称等
- 移除旧的时间字段created_time last_mod_time
- 添加新的时间字段creation_time last_modify_time
- 修改多个模型字段的属性 verbose_namedefault 最大长度等
Attributes:
dependencies (list): 指定当前迁移所依赖的其他迁移文件
operations (list): 包含所有数据库变更操作的列表
"""
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
# mk:更新 Article 模型的默认排序规则和其他元信息
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# mk:更新 Category 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# mk:更新 Links 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# mk:更新 Sidebar 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# mk:更新 Tag 模型的排序规则及显示名称
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# mk:删除 Article 模型中已废弃的时间字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# mk:删除 Category 模型中已废弃的时间字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# mk:删除 Links 模型中已废弃的时间字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# mk:删除 Sidebar 模型中已废弃的时间字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# mk:删除 Tag 模型中已废弃的时间字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# mk:为 Article 模型添加新的创建时间和最后修改时间字段
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#mk: 为 Category 模型添加新的创建时间和最后修改时间字段
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# mk:为 Links 模型添加新的创建时间字段
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# mk:为 Sidebar 模型添加新的创建时间字段
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# mk:为 Tag 模型添加新的创建时间和最后修改时间字段
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# mk:修改 Article 模型各字段定义以增强可读性和规范性
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# mk:修改 BlogSettings 模型相关配置项字段定义
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# mk:修改 Category 模型字段定义
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# mk:修改 Links 模型字段定义
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
# mk:修改 Sidebar 模型字段定义
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# mk:修改 Tag 模型字段定义
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
migrations.RemoveField(
model_name='links',
name='created_time',
),
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -0,0 +1,26 @@
# mk:Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
"""
mk:
Django数据库迁移类用于修改BlogSettings模型的显示选项配置
该迁移类继承自Django的Migration基类主要作用是更新blogsettings模型的元数据选项
设置更友好的模型名称显示
"""
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [
# mk:修改BlogSettings模型的显示选项设置单数和复数形式的可读名称
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -18,6 +18,17 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""
mk:
友情链接显示类型枚举
属性:
I: 首页显示
L: 列表页显示
P: 文章页显示
A: 全部页面显示
S: 幻灯片显示
"""
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
@ -26,11 +37,31 @@ class LinkShowType(models.TextChoices):
class BaseModel(models.Model):
"""
mk:
基础模型类提供通用字段和方法
属性:
id: 主键ID
creation_time: 创建时间
last_modify_time: 最后修改时间
"""
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
"""
mk:
保存模型实例
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
@ -45,6 +76,13 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
"""
mk:
获取完整的URL地址
Returns:
str: 完整的URL地址
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
@ -55,11 +93,35 @@ class BaseModel(models.Model):
@abstractmethod
def get_absolute_url(self):
"""
mk:
抽象方法获取绝对URL地址
Returns:
str: 绝对URL地址
"""
pass
class Article(BaseModel):
"""文章"""
"""
mk:
文章模型类
属性:
title: 标题
body: 正文内容
pub_time: 发布时间
status: 状态草稿/发布
comment_status: 评论状态开启/关闭
type: 类型文章/页面
views: 浏览量
author: 作者
article_order: 排序
show_toc: 是否显示目录
category: 分类
tags: 标签
"""
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
@ -106,9 +168,23 @@ class Article(BaseModel):
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
"""
mk:
将正文内容转换为字符串
Returns:
str: 正文内容字符串
"""
return self.body
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 文章标题
"""
return self.title
class Meta:
@ -118,6 +194,13 @@ class Article(BaseModel):
get_latest_by = 'id'
def get_absolute_url(self):
"""
mk:
获取文章详情页的绝对URL
Returns:
str: 文章详情页URL
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -127,19 +210,48 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
mk:
获取分类树结构
Returns:
list: 分类名称和URL的元组列表
"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
"""
mk:
保存文章实例
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
super().save(*args, **kwargs)
def viewed(self):
"""
mk:
增加文章浏览量并保存
"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""
mk:
获取文章评论列表带缓存功能
Returns:
QuerySet: 评论查询集
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -152,24 +264,48 @@ class Article(BaseModel):
return comments
def get_admin_url(self):
"""
mk:
获取后台管理URL
Returns:
str: 后台管理URL
"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
def next_article(self):
"""
mk:
获取下一篇已发布的文章
Returns:
Article|None: 下一篇文章对象或None
"""
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
"""
mk:
获取上一篇已发布的文章
Returns:
Article|None: 上一篇文章对象或None
"""
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
mk:
从文章正文中提取第一张图片的URL
Returns:
str: 第一张图片的URL如果没有找到则返回空字符串
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
@ -178,7 +314,16 @@ class Article(BaseModel):
class Category(BaseModel):
"""文章分类"""
"""
mk:
文章分类模型类
属性:
name: 分类名称
parent_category: 父级分类
slug: URL别名
index: 排序索引
"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self',
@ -195,18 +340,35 @@ class Category(BaseModel):
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""
mk:
获取分类详情页的绝对URL
Returns:
str: 分类详情页URL
"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 分类名称
"""
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
mk:
递归获取分类目录的父级分类树
Returns:
list: 包含当前分类及其所有父级分类的列表
"""
categorys = []
@ -221,8 +383,11 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
mk:
获取当前分类目录的所有子分类
Returns:
list: 包含当前分类及其所有子分类的列表
"""
categorys = []
all_categorys = Category.objects.all()
@ -241,18 +406,46 @@ class Category(BaseModel):
class Tag(BaseModel):
"""文章标签"""
"""
mk:
文章标签模型类
属性:
name: 标签名称
slug: URL别名
"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 标签名称
"""
return self.name
def get_absolute_url(self):
"""
mk:
获取标签详情页的绝对URL
Returns:
str: 标签详情页URL
"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
"""
mk:
获取使用该标签的文章数量
Returns:
int: 文章数量
"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
@ -262,8 +455,19 @@ class Tag(BaseModel):
class Links(models.Model):
"""友情链接"""
"""
mk:
友情链接模型类
属性:
name: 链接名称
link: 链接地址
sequence: 排序
is_enable: 是否启用
show_type: 显示类型
creation_time: 创建时间
last_mod_time: 最后修改时间
"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
@ -283,11 +487,29 @@ class Links(models.Model):
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 链接名称
"""
return self.name
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
"""
mk:
侧边栏模型类用于展示HTML内容
属性:
name: 标题
content: 内容
sequence: 排序
is_enable: 是否启用
creation_time: 创建时间
last_mod_time: 最后修改时间
"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
@ -301,11 +523,41 @@ class SideBar(models.Model):
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 侧边栏标题
"""
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
"""
mk:
博客配置模型类
属性:
site_name: 网站名称
site_description: 网站描述
site_seo_description: SEO描述
site_keywords: SEO关键词
article_sub_length: 文章摘要长度
sidebar_article_count: 侧边栏文章数量
sidebar_comment_count: 侧边栏评论数量
article_comment_count: 文章评论数量
show_google_adsense: 是否显示Google广告
google_adsense_codes: Google广告代码
open_site_comment: 是否开启网站评论
global_header: 公共头部代码
global_footer: 公共尾部代码
beian_code: 备案号
analytics_code: 网站统计代码
show_gongan_code: 是否显示公安备案号
gongan_beiancode: 公安备案号
comment_need_review: 评论是否需要审核
"""
site_name = models.CharField(
_('site name'),
max_length=200,
@ -364,13 +616,38 @@ class BlogSettings(models.Model):
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 网站名称
"""
return self.site_name
def clean(self):
"""
mk:
数据验证确保只存在一个配置实例
Raises:
ValidationError: 当已存在配置实例时抛出异常
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
mk:
保存配置并清除缓存
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -0,0 +1,39 @@
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
"""
mk:
文章搜索索引类
用于为Article模型创建搜索引擎索引继承自haystack的SearchIndex和Indexable类
"""
# 定义搜索文档的文本字段document=True表示这是主要的搜索字段
# use_template=True表示使用模板来定义索引内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""
mk:
获取索引关联的模型类
Returns:
Article: 返回Article模型类
"""
return Article
def index_queryset(self, using=None):
"""
mk:
获取需要建立索引的查询集
Args:
using (str, optional): 数据库别名默认为None
Returns:
QuerySet: 返回状态为'p'(已发布)的文章查询集
"""
return self.get_model().objects.filter(status='p')

@ -1881,7 +1881,7 @@ img#wpstats {
}
}
/* Minimum width of 960 pixels. */
/* mk:Minimum width of 960 pixels. */
@media screen and (min-width: 960px) {
body {
background-color: #e6e6e6;
@ -2145,14 +2145,14 @@ div {
word-break: break-all;
}
/* 评论整体布局 - 使用相对定位实现头像左侧布局 */
/* mk:评论整体布局 - 使用相对定位实现头像左侧布局 */
.commentlist .comment-body {
position: relative;
padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px; /* 确保有足够高度容纳头像 */
}
/* 评论作者信息 - 用户名和时间在同一行 */
/*mk: 评论作者信息 - 用户名和时间在同一行 */
.commentlist .comment-author {
display: inline-block;
margin: 0 10px 5px 0;
@ -2173,7 +2173,7 @@ div {
line-height: 22px;
}
/* 头像样式 - 绝对定位到左侧 */
/*mk: 头像样式 - 绝对定位到左侧 */
.commentlist .comment-author .avatar {
position: absolute !important;
left: -60px; /* 定位到容器左侧 */
@ -2187,7 +2187,7 @@ div {
border: 1px solid #ddd;
}
/* 评论作者名称样式 */
/*mk: 评论作者名称样式 */
.commentlist .comment-author .fn {
display: inline;
margin: 0;
@ -2205,7 +2205,7 @@ div {
text-decoration: underline;
}
/* 评论内容样式 */
/* mk:评论内容样式 */
.commentlist .comment-body p {
margin: 5px 0 10px 0;
line-height: 1.5;
@ -2222,7 +2222,7 @@ div {
display: none;
}
/* 通用头像样式 */
/* mk:通用头像样式 */
.commentlist .avatar {
width: 48px !important;
height: 48px !important;
@ -2266,12 +2266,12 @@ div {
font-style: normal;
}
/* pings */
/* mk:pings */
.pinglist li {
padding-left: 0;
}
/* comment text */
/*mk:comment text */
.commentlist .comment-body p {
margin-bottom: 8px;
color: #777;
@ -2294,7 +2294,7 @@ div {
list-style: square;
}
/* post author & admin comment */
/*mk: post author & admin comment */
.commentlist li.bypostauthor > .comment-body:after,
.commentlist li.comment-author-admin > .comment-body:after {
display: block;
@ -2331,7 +2331,7 @@ div {
border-radius: 100%;
}
/* child comment */
/* mk:child comment */
.commentlist li ul {
}
@ -2340,42 +2340,42 @@ div {
padding-left: 48px;
}
/* 嵌套评论整体布局 */
/*mk: 嵌套评论整体布局 */
.commentlist li li .comment-body {
padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px; /* 确保有足够高度容纳头像 */
padding-left: 60px; /* mk:为48px头像 + 12px间距留出空间 */
min-height: 48px; /* mk:确保有足够高度容纳头像 */
}
/* 嵌套评论作者信息 */
/*mk: 嵌套评论作者信息 */
.commentlist li li .comment-author {
display: inline-block;
margin: 0 8px 5px 0;
font-size: 12px; /* 稍小一点 */
font-size: 12px; /*mk: 稍小一点 */
}
.commentlist li li .comment-meta {
display: inline-block;
margin: 0 0 8px 0;
font-size: 11px; /* 稍小一点 */
font-size: 11px; /*mk: 稍小一点 */
color: #666;
}
/* 评论容器整体左移 - 使用更高优先级 */
/* mk:评论容器整体左移 - 使用更高优先级 */
#comments #commentlist-container.comment-tab {
margin-left: -15px !important; /* 在小屏幕上向左移动15px */
padding-left: 0 !important; /* 移除左内边距 */
position: relative !important; /* 确保定位正确 */
margin-left: -15px !important; /* mk:在小屏幕上向左移动15px */
padding-left: 0 !important; /* mk:移除左内边距 */
position: relative !important; /*mk: 确保定位正确 */
}
/* 在较大屏幕上进一步左移 */
/* mk:在较大屏幕上进一步左移 */
@media screen and (min-width: 600px) {
#comments #commentlist-container.comment-tab {
margin-left: -30px !important; /* 在大屏幕上向左移动30px */
margin-left: -30px !important; /* mk:在大屏幕上向左移动30px */
}
/* 响应式设计下的评论布局 - 保持48px头像 */
/*mk: 响应式设计下的评论布局 - 保持48px头像 */
.commentlist .comment-body {
padding-left: 60px !important; /* 为48px头像 + 12px间距留出空间 */
padding-left: 60px !important; /* mk:为48px头像 + 12px间距留出空间 */
min-height: 48px !important;
}
@ -2389,14 +2389,14 @@ div {
margin: 0 0 8px 0 !important;
}
/* 响应式设计下头像保持48px */
/* mk:响应式设计下头像保持48px */
.commentlist .comment-author .avatar {
left: -60px !important;
width: 48px !important;
height: 48px !important;
}
/* 嵌套评论在响应式设计下也保持48px头像 */
/* mk:嵌套评论在响应式设计下也保持48px头像 */
.commentlist li li .comment-body {
padding-left: 60px !important;
min-height: 48px !important;
@ -2409,10 +2409,10 @@ div {
}
}
/* 嵌套评论头像 */
/* mk:嵌套评论头像 */
.commentlist li li .comment-author .avatar {
position: absolute !important;
left: -60px; /* 定位到容器左侧 */
left: -60px; /* mk:定位到容器左侧 */
top: 0;
width: 48px !important;
height: 48px !important;
@ -2423,7 +2423,7 @@ div {
border: 1px solid #ddd;
}
/* comments : nav
/* mk:comments : nav
/* ------------------------------------ */
.comments-nav {
margin-bottom: 20px;
@ -2441,7 +2441,7 @@ div {
float: right;
}
/* comments : form
/* mk: comments : form
/* ------------------------------------ */
.logged-in-as,
.comment-notes,
@ -2626,11 +2626,12 @@ li #reply-title {
}
/* =============================================================================
mk:
============================================================================= */
/* 评论容器基础样式 */
/* mk:评论容器基础样式 */
.comment-body {
overflow-wrap: break-word;
word-wrap: break-word;
@ -2639,7 +2640,7 @@ li #reply-title {
box-sizing: border-box;
}
/* 修复评论中的代码块溢出 */
/* mk:修复评论中的代码块溢出 */
.comment-content pre,
.comment-body pre {
white-space: pre-wrap !important;
@ -2656,7 +2657,7 @@ li #reply-title {
margin: 10px 0;
}
/* 修复评论中的行内代码 */
/* mk:修复评论中的行内代码 */
.comment-content code,
.comment-body code {
word-wrap: break-word !important;
@ -2667,7 +2668,7 @@ li #reply-title {
vertical-align: top;
}
/* 修复评论中的长链接 */
/* mk:修复评论中的长链接 */
.comment-content a,
.comment-body a {
word-wrap: break-word !important;
@ -2676,7 +2677,7 @@ li #reply-title {
max-width: 100%;
}
/* 修复评论段落 */
/* mk:修复评论段落 */
.comment-content p,
.comment-body p {
word-wrap: break-word !important;
@ -2685,7 +2686,7 @@ li #reply-title {
margin: 10px 0;
}
/* 特殊处理代码高亮块 - 关键修复! */
/* mk:特殊处理代码高亮块 - 关键修复! */
.comment-content .codehilite,
.comment-body .codehilite {
max-width: 100% !important;
@ -2720,7 +2721,7 @@ li #reply-title {
box-sizing: border-box;
}
/* 修复代码高亮中的span标签 */
/* mk:修复代码高亮中的span标签 */
.comment-content .codehilite span,
.comment-body .codehilite span {
word-wrap: break-word !important;
@ -2730,7 +2731,7 @@ li #reply-title {
max-width: 100%;
}
/* 针对特定的代码高亮类 */
/* mk:针对特定的代码高亮类 */
.comment-content .codehilite .kt,
.comment-content .codehilite .nf,
.comment-content .codehilite .n,
@ -2743,7 +2744,7 @@ li #reply-title {
overflow-wrap: break-word !important;
}
/* 搜索结果高亮样式 */
/* mk:搜索结果高亮样式 */
.search-result {
margin-bottom: 30px;
padding: 20px;
@ -2785,7 +2786,7 @@ li #reply-title {
margin: 10px 0;
}
/* 搜索关键词高亮 */
/* mk:搜索关键词高亮 */
.search-excerpt em,
.search-result .entry-title em {
background-color: #fff3cd;
@ -2817,14 +2818,14 @@ li #reply-title {
overflow-wrap: break-word;
}
/* 修复评论列表项 */
/* mk:修复评论列表项 */
.commentlist li {
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
/* 确保评论内容不超出容器 */
/* mk:确保评论内容不超出容器 */
.commentlist .comment-body {
max-width: calc(100% - 20px); /* 留出一些边距 */
margin-left: 10px;
@ -2833,21 +2834,21 @@ li #reply-title {
word-wrap: break-word;
}
/* 重要:限制评论列表项的最大宽度 */
/* mk:重要:限制评论列表项的最大宽度 */
.commentlist li[style*="margin-left"] {
max-width: calc(100% - 2rem) !important;
overflow: hidden;
box-sizing: border-box;
}
/* 特别处理深层嵌套的评论 */
/* mk:特别处理深层嵌套的评论 */
.commentlist li[style*="margin-left: 3rem"],
.commentlist li[style*="margin-left: 6rem"],
.commentlist li[style*="margin-left: 9rem"] {
max-width: calc(100% - 1rem) !important;
}
/* 移动端优化 */
/*mk: 移动端优化 */
@media (max-width: 768px) {
.comment-content pre,
.comment-body pre {
@ -2862,14 +2863,14 @@ li #reply-title {
margin-right: 5px;
}
/* 移动端评论缩进调整 */
/* mk:移动端评论缩进调整 */
.commentlist li[style*="margin-left"] {
margin-left: 1rem !important;
max-margin-left: 2rem !important;
}
}
/* 防止表格溢出 */
/* mk:防止表格溢出 */
.comment-content table,
.comment-body table {
max-width: 100%;
@ -2878,14 +2879,14 @@ li #reply-title {
white-space: nowrap;
}
/* 修复图片溢出 */
/* mk:修复图片溢出 */
.comment-content img,
.comment-body img {
max-width: 100% !important;
height: auto !important;
}
/* 修复引用块 */
/* mk:修复引用块 */
.comment-content blockquote,
.comment-body blockquote {
max-width: 100%;

@ -1,174 +1,12 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
/*
mk:
* Open Sans /300400600
* @font-face 西
* 使 WOFF2 unicode-range
* font-display: swap 使
*/
/* mk:vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -178,7 +16,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
/*mk: latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -188,7 +26,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
/*mk: latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -198,7 +36,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
/*mk: cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -208,7 +46,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
/*mk: cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -218,7 +56,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
/*mk: greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -228,7 +66,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
/*mk: greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -238,7 +76,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
/*mk: hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -248,7 +86,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
/*mk: math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -258,7 +96,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
/*mk: symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -268,7 +106,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
/*mk: vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -278,7 +116,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
/*mk: latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -288,7 +126,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
/*mk: latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
@ -298,7 +136,7 @@
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
/*mk: cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -308,17 +146,17 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
/*mk: cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
/*mk: greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -328,7 +166,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
/*mk: greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -338,7 +176,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
/*mk: hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -348,7 +186,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
/*mk: math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -358,7 +196,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
/*mk: symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -368,7 +206,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
/*mk: vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -378,7 +216,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
/*mk: latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -388,7 +226,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
/*mk: latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -398,7 +236,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
/*mk: cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -408,7 +246,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
/*mk: cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -418,7 +256,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
/*mk: greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -428,7 +266,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
/*mk: greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -438,7 +276,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
/*mk: hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -448,7 +286,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
/*mk: math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -458,7 +296,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
/*mk: symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -468,7 +306,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
/*mk: vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -478,7 +316,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
/*mk: latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -488,7 +326,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
/*mk: latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -498,7 +336,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
/*mk: cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -508,7 +346,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
/*mk: cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -518,7 +356,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
/* mk:greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -528,7 +366,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
/*mk: greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -538,7 +376,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
/* mk:hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -548,7 +386,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
/* mk:math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -558,7 +396,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
/* mk:symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -568,7 +406,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
/* mk:vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -578,7 +416,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
/* mk:latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -588,7 +426,7 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
/* mk:latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -598,3 +436,4 @@
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -3,6 +3,11 @@
*/
/**
mk:
* 执行回复操作设置父评论ID并将评论表单移动到指定位置
* @param {string|number} parentid - 父评论的ID
*/
function do_reply(parentid) {
console.log(parentid);
$("#id_parent_comment_id").val(parentid)
@ -11,6 +16,10 @@ function do_reply(parentid) {
$("#cancel_comment").show();
}
/**
mk:
* 取消回复操作重置评论表单到初始状态
*/
function cancel_reply() {
$("#reply-title").show();
$("#cancel_comment").hide();
@ -18,23 +27,34 @@ function cancel_reply() {
$("#commentform").appendTo($("#respond"));
}
// mk:初始化页面加载进度条
NProgress.start();
NProgress.set(0.4);
//Increment
var interval = setInterval(function () {
NProgress.inc();
}, 1000);
// mk:页面加载完成后停止进度条
$(document).ready(function () {
NProgress.done();
clearInterval(interval);
});
/** 侧边栏回到顶部 */
/**mk: 侧边栏回到顶部 */
var rocket = $('#rocket');
//mk: 监听窗口滚动事件,控制回到顶部按钮的显示
$(window).on('scroll', debounce(slideTopSet, 300));
/**
mk:
* 防抖函数用于限制函数执行频率
* @param {Function} func - 需要防抖的函数
* @param {number} wait - 延迟执行的时间间隔毫秒
* @returns {Function} 返回防抖后的函数
*/
function debounce(func, wait) {
var timeout;
return function () {
@ -43,6 +63,10 @@ function debounce(func, wait) {
};
}
/**
mk:
* 根据滚动位置控制回到顶部按钮的显示状态
*/
function slideTopSet() {
var top = $(document).scrollTop();
@ -53,12 +77,15 @@ function slideTopSet() {
}
}
// mk:点击回到顶部按钮时执行动画滚动到顶部
$(document).on('click', '#rocket', function (event) {
rocket.addClass('move');
$('body, html').animate({
scrollTop: 0
}, 800);
});
// mk:动画结束后的清理处理
$(document).on('animationEnd', function () {
setTimeout(function () {
rocket.removeClass('move');
@ -72,6 +99,7 @@ $(document).on('webkitAnimationEnd', function () {
});
// mk:页面加载完成后为所有回复链接绑定点击事件
window.onload = function () {
var replyLinks = document.querySelectorAll(".comment-reply-link");
for (var i = 0; i < replyLinks.length; i++) {
@ -82,10 +110,10 @@ window.onload = function () {
}
};
// $(document).ready(function () {
//mk: $(document).ready(function () {
// var form = $('#i18n-form');
// var selector = $('.i18n-select');
// selector.on('change', function () {
// form.submit();
// });
// });
// });

@ -0,0 +1,60 @@
/*
HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
/**
mk:
* 获取HTML5元素列表
* @returns {Array|string} 返回元素名称数组或字符串
*/
(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}
/**
mk:
* 获取与文档关联的缓存对象
* @param {Object} a - 文档对象
* @returns {Object} 返回对应的缓存对象
*/
function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}
/**
mk:
* 创建HTML元素
* @param {string} a - 元素标签名
* @param {Object} b - 文档对象可选
* @param {Object} c - 缓存对象可选
* @returns {Object} 返回创建的元素节点
*/
function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}
/**
mk:
* 重写文档的createElement和createDocumentFragment方法以支持HTML5元素
* @param {Object} a - 文档对象
* @param {Object} b - 缓存配置对象
*/
function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}
/**
mk:
* 初始化并应用HTML5 Shiv到指定文档
* @param {Object} a - 文档对象可选默认为当前文档
* @returns {Object} 返回处理后的文档对象
*/
function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>";
c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}
// mk:初始化变量和配置
var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;
// mk:检测浏览器是否原生支持未知元素和相关API
(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();
// mk:定义HTML5 Shiv的核心配置和公共接口
var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};
// mk:应用HTML5 Shiv到当前文档
l.html5=e;q(f)})(this,document);

@ -0,0 +1,95 @@
/**
mk:
* fadeToggle - 切换元素的透明度动画效果
*
* @param {string} e - 动画类型标识符
* @param {object} r - 包含动画属性的对象 opacity
* @returns {function} 返回一个用于执行动画的函数
*/
fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}},
/**
mk:
* S.timers - 存储所有活动定时器的数组
*/
S.timers=[],
/**
mk:
* S.fx.tick - 执行所有活动动画帧更新
* 遍历并执行定时器队列中的每一项并清理已完成或无效的定时器
*/
S.fx.tick=function(){
var e,t=0,n=S.timers;
for(Ze=Date.now();t<n.length;t++)
(e=n[t])()||n[t]!==e||n.splice(t--,1);
n.length||S.fx.stop(),
Ze=void 0
},
/**
mk:
* S.fx.timer - 将新的动画定时器加入到定时器队列中
*
* @param {function} e - 要添加的定时器回调函数
*/
S.fx.timer=function(e){
S.timers.push(e),
S.fx.start()
},
/**
mk:
* S.fx.interval - 设置动画刷新间隔时间毫秒
*/
S.fx.interval=13,
/**
mk:
* S.fx.start - 启动动画循环处理机制
* 若尚未启动则初始化动画循环
*/
S.fx.start=function(){
et||(et=!0,ot())
},
/**
mk:
* S.fx.stop - 停止动画循环处理机制
*/
S.fx.stop=function(){
et=null
},
/**
mk:
* S.fx.speeds - 定义预设动画速度常量
* slow: 慢速动画持续时间
* fast: 快速动画持续时间
* _default: 默认动画持续时间
*/
S.fx.speeds={
slow:600,
fast:200,
_default:400
},
/**
mk:
* S.fn.delay - 在动画队列中插入延迟操作
*
* @param {number} r - 延迟的时间长度单位毫秒
* @param {string} e - 动画队列名称默认为 "fx"
* @returns {object} jQuery 对象本身以支持链式调用
*/
S.fn.delay=function(r,e){
return r=S.fx&&S.fx.speeds[r]||r,
e=e||"fx",
this.queue(e,function(e,t){
var n=C.setTimeout(e,r);
t.stop=function(){
C.clearTimeout(n)
}
})
};

@ -1,4 +1,5 @@
/**
mk:
* MathJax 智能加载器
* 检测页面是否包含数学公式如果有则动态加载和配置MathJax
*/
@ -6,6 +7,7 @@
'use strict';
/**
mk:
* 检测页面是否包含数学公式
* @returns {boolean} 是否包含数学公式
*/
@ -16,34 +18,35 @@
}
/**
mk:
* 配置MathJax
*/
function configureMathJax() {
window.MathJax = {
tex: {
// 行内公式和块级公式分隔符
// mk:行内公式和块级公式分隔符
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
// 处理转义字符和LaTeX环境
// mk:处理转义字符和LaTeX环境
processEscapes: true,
processEnvironments: true,
// 自动换行
// mk:自动换行
tags: 'ams'
},
options: {
// 跳过这些HTML标签避免处理代码块等
// mk:跳过这些HTML标签避免处理代码块等
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
// CSS类控制
// mk:CSS类控制
ignoreHtmlClass: 'tex2jax_ignore',
processHtmlClass: 'tex2jax_process'
},
// 启动配置
// mk:启动配置
startup: {
ready() {
console.log('MathJax配置完成开始初始化...');
MathJax.startup.defaultReady();
// 处理特定区域的数学公式
// mk:处理特定区域的数学公式
const contentEl = document.getElementById('content');
const commentsEl = document.getElementById('comments');
@ -55,17 +58,17 @@
promises.push(MathJax.typesetPromise([commentsEl]));
}
// 等待所有渲染完成
// mk:等待所有渲染完成
Promise.all(promises).then(() => {
console.log('MathJax渲染完成');
// 触发自定义事件通知其他脚本MathJax已就绪
// mk:触发自定义事件通知其他脚本MathJax已就绪
document.dispatchEvent(new CustomEvent('mathjaxReady'));
}).catch(error => {
console.error('MathJax渲染失败:', error);
});
}
},
// 输出配置
// mk:输出配置
chtml: {
scale: 1,
minScale: 0.5,
@ -77,6 +80,7 @@
}
/**
mk:
* 加载MathJax库
*/
function loadMathJax() {
@ -109,18 +113,19 @@
}
/**
mk:
* 初始化函数
*/
function init() {
// 等待DOM完全加载
// mk:等待DOM完全加载
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
// 检测是否需要加载MathJax
// mk:检测是否需要加载MathJax
if (hasMathFormulas()) {
// 先配置,再加载
// mk:先配置,再加载
configureMathJax();
loadMathJax();
} else {
@ -128,7 +133,7 @@
}
}
// 提供重新渲染的全局方法,供动态内容使用
// mk:提供重新渲染的全局方法,供动态内容使用
window.rerenderMathJax = function(element) {
if (window.MathJax && window.MathJax.typesetPromise) {
const target = element || document.body;
@ -137,6 +142,6 @@
return Promise.resolve();
};
// 启动初始化
// mk:启动初始化
init();
})();

@ -1,6 +1,11 @@
/**
mk:
* Handles toggling the navigation menu for small screens and
* accessibility for submenu items.
*
* This self-invoking function manages the mobile navigation toggle behavior.
* It adds click event to the menu button to show/hide the navigation menu
* by toggling CSS classes.
*/
( function() {
var nav = document.getElementById( 'site-navigation' ), button, menu;
@ -14,7 +19,7 @@
return;
}
// Hide button if menu is missing or empty.
// mk:Hide button if menu is missing or empty.
if ( ! menu || ! menu.childNodes.length ) {
button.style.display = 'none';
return;
@ -35,12 +40,23 @@
};
} )();
// Better focus for hidden submenu items for accessibility.
/**
mk:
* Enhances focus handling for submenu items to improve accessibility
* and handles touch events for mobile devices.
*
* @param {Object} $ - jQuery object
*
* This self-invoking function improves the accessibility of navigation menus
* by adding focus and blur event handlers to menu items. It also handles
* touch events for mobile devices to properly display submenus.
*/
( function( $ ) {
$( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() {
$( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' );
} );
// mk:Handle touch events for mobile devices to properly display submenus
if ( 'ontouchstart' in window ) {
$('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) {
var el = $( this ).parent( 'li' );

@ -1,4 +1,4 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
/* mk:NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
;(function(root, factory) {
@ -31,6 +31,7 @@
};
/**
mk:
* Updates configuration.
*
* NProgress.configure({
@ -48,12 +49,14 @@
};
/**
mk:
* Last number.
*/
NProgress.status = null;
/**
mk:
* Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
*
* NProgress.set(0.4);
@ -71,13 +74,13 @@
speed = Settings.speed,
ease = Settings.easing;
progress.offsetWidth; /* Repaint */
progress.offsetWidth; /*mk:Repaint */
queue(function(next) {
// Set positionUsing if it hasn't already been set
// mk:Set positionUsing if it hasn't already been set
if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
// Add transition
// mk:Add transition
css(bar, barPositionCSS(n, speed, ease));
if (n === 1) {
@ -111,6 +114,7 @@
};
/**
mk:
* Shows the progress bar.
* This is the same as setting the status to 0%, except that it doesn't go backwards.
*
@ -134,6 +138,7 @@
};
/**
mk:
* Hides the progress bar.
* This is the *sort of* the same as setting the status to 100%, with the
* difference being `done()` makes some placebo effect of some realistic motion.
@ -152,6 +157,7 @@
};
/**
mk:
* Increments by a random amount.
*/
@ -181,6 +187,7 @@
};
/**
mk:
* Waits for all supplied jQuery promises and
* increases the progress as the promises resolve.
*
@ -217,6 +224,7 @@
})();
/**
mk:
* (Internal) renders the progress bar markup based on the `template`
* setting.
*/
@ -254,6 +262,7 @@
};
/**
mk:
* Removes the element. Opposite of render().
*/
@ -265,6 +274,7 @@
};
/**
mk:
* Checks if the progress bar is rendered.
*/
@ -273,32 +283,34 @@
};
/**
mk:
* Determine which positioning CSS rule to use.
*/
NProgress.getPositioningCSS = function() {
// Sniff on document.body.style
// mk:Sniff on document.body.style
var bodyStyle = document.body.style;
// Sniff prefixes
// mk:Sniff prefixes
var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
('MozTransform' in bodyStyle) ? 'Moz' :
('msTransform' in bodyStyle) ? 'ms' :
('OTransform' in bodyStyle) ? 'O' : '';
if (vendorPrefix + 'Perspective' in bodyStyle) {
// Modern browsers with 3D support, e.g. Webkit, IE10
//mk: Modern browsers with 3D support, e.g. Webkit, IE10
return 'translate3d';
} else if (vendorPrefix + 'Transform' in bodyStyle) {
// Browsers without 3D support, e.g. IE9
// mk:Browsers without 3D support, e.g. IE9
return 'translate';
} else {
// Browsers without translate() support, e.g. IE7-8
// mk:Browsers without translate() support, e.g. IE7-8
return 'margin';
}
};
/**
mk:
* Helpers
*/
@ -309,6 +321,7 @@
}
/**
mk:
* (Internal) converts a percentage (`0..1`) to a bar translateX
* percentage (`-100%..0%`).
*/
@ -319,6 +332,7 @@
/**
mk:
* (Internal) returns the correct CSS for changing the bar's
* position given an n percentage, and speed and ease from Settings
*/
@ -340,6 +354,7 @@
}
/**
mk:
* (Internal) Queues a function to be executed.
*/
@ -360,6 +375,7 @@
})();
/**
mk:
* (Internal) Applies css properties to an element, similar to the jQuery
* css method.
*
@ -419,6 +435,7 @@
})();
/**
mk:
* (Internal) Determines if an element or space separated list of class names contains a class name.
*/
@ -427,7 +444,7 @@
return list.indexOf(' ' + name + ' ') >= 0;
}
/**
/**mk:
* (Internal) Adds a class to an element.
*/
@ -442,6 +459,7 @@
}
/**
mk:
* (Internal) Removes a class from an element.
*/
@ -451,14 +469,15 @@
if (!hasClass(element, name)) return;
// Replace the class name.
// mk:Replace the class name.
newList = oldList.replace(' ' + name + ' ', ' ');
// Trim the opening and closing spaces.
// mk:Trim the opening and closing spaces.
element.className = newList.substring(1, newList.length - 1);
}
/**
mk:
* (Internal) Gets a space separated list of the class names on the element.
* The list is wrapped with a single space on each end to facilitate finding
* matches within the list.
@ -469,6 +488,7 @@
}
/**
mk:
* (Internal) Removes an element from the DOM.
*/

@ -10,284 +10,284 @@
color: #177500
}
/* Comment */
/*mk: Comment */
.codehilite .err {
color: #000000
}
/* Error */
/*mk: Error */
.codehilite .k {
color: #A90D91
}
/* Keyword */
/*mk: Keyword */
.codehilite .l {
color: #1C01CE
}
/* Literal */
/*mk: Literal */
.codehilite .n {
color: #000000
}
/* Name */
/*mk: Name */
.codehilite .o {
color: #000000
}
/* Operator */
/*mk: Operator */
.codehilite .ch {
color: #177500
}
/* Comment.Hashbang */
/*mk: Comment.Hashbang */
.codehilite .cm {
color: #177500
}
/* Comment.Multiline */
/*mk: Comment.Multiline */
.codehilite .cp {
color: #633820
}
/* Comment.Preproc */
/*mk: Comment.Preproc */
.codehilite .cpf {
color: #177500
}
/* Comment.PreprocFile */
/*mk: Comment.PreprocFile */
.codehilite .c1 {
color: #177500
}
/* Comment.Single */
/*mk: Comment.Single */
.codehilite .cs {
color: #177500
}
/* Comment.Special */
/*mk: Comment.Special */
.codehilite .kc {
color: #A90D91
}
/* Keyword.Constant */
/*mk: Keyword.Constant */
.codehilite .kd {
color: #A90D91
}
/* Keyword.Declaration */
/*mk: Keyword.Declaration */
.codehilite .kn {
color: #A90D91
}
/* Keyword.Namespace */
/*mk: Keyword.Namespace */
.codehilite .kp {
color: #A90D91
}
/* Keyword.Pseudo */
/*mk: Keyword.Pseudo */
.codehilite .kr {
color: #A90D91
}
/* Keyword.Reserved */
/*mk: Keyword.Reserved */
.codehilite .kt {
color: #A90D91
}
/* Keyword.Type */
/*mk: Keyword.Type */
.codehilite .ld {
color: #1C01CE
}
/* Literal.Date */
/*mk: Literal.Date */
.codehilite .m {
color: #1C01CE
}
/* Literal.Number */
/*mk: Literal.Number */
.codehilite .s {
color: #C41A16
}
/* Literal.String */
/*mk: Literal.String */
.codehilite .na {
color: #836C28
}
/* Name.Attribute */
/*mk: Name.Attribute */
.codehilite .nb {
color: #A90D91
}
/* Name.Builtin */
/*mk: Name.Builtin */
.codehilite .nc {
color: #3F6E75
}
/* Name.Class */
/*mk: Name.Class */
.codehilite .no {
color: #000000
}
/* Name.Constant */
/*mk: Name.Constant */
.codehilite .nd {
color: #000000
}
/* Name.Decorator */
/*mk: Name.Decorator */
.codehilite .ni {
color: #000000
}
/* Name.Entity */
/*mk: Name.Entity */
.codehilite .ne {
color: #000000
}
/* Name.Exception */
/*mk: Name.Exception */
.codehilite .nf {
color: #000000
}
/* Name.Function */
/*mk: Name.Function */
.codehilite .nl {
color: #000000
}
/* Name.Label */
/*mk: Name.Label */
.codehilite .nn {
color: #000000
}
/* Name.Namespace */
/*mk: Name.Namespace */
.codehilite .nx {
color: #000000
}
/* Name.Other */
/*mk: Name.Other */
.codehilite .py {
color: #000000
}
/* Name.Property */
/*mk: Name.Property */
.codehilite .nt {
color: #000000
}
/* Name.Tag */
/*mk: Name.Tag */
.codehilite .nv {
color: #000000
}
/* Name.Variable */
/*mk: Name.Variable */
.codehilite .ow {
color: #000000
}
/* Operator.Word */
/*mk: Operator.Word */
.codehilite .mb {
color: #1C01CE
}
/* Literal.Number.Bin */
/*mk: Literal.Number.Bin */
.codehilite .mf {
color: #1C01CE
}
/* Literal.Number.Float */
/*mk: Literal.Number.Float */
.codehilite .mh {
color: #1C01CE
}
/* Literal.Number.Hex */
/*mk: Literal.Number.Hex */
.codehilite .mi {
color: #1C01CE
}
/* Literal.Number.Integer */
/*mk: Literal.Number.Integer */
.codehilite .mo {
color: #1C01CE
}
/* Literal.Number.Oct */
/*mk: Literal.Number.Oct */
.codehilite .sb {
color: #C41A16
}
/* Literal.String.Backtick */
/*mk: Literal.String.Backtick */
.codehilite .sc {
color: #2300CE
}
/* Literal.String.Char */
/*mk: Literal.String.Char */
.codehilite .sd {
color: #C41A16
}
/* Literal.String.Doc */
/*mk: Literal.String.Doc */
.codehilite .s2 {
color: #C41A16
}
/* Literal.String.Double */
/*mk: Literal.String.Double */
.codehilite .se {
color: #C41A16
}
/* Literal.String.Escape */
/*mk: Literal.String.Escape */
.codehilite .sh {
color: #C41A16
}
/* Literal.String.Heredoc */
/*mk: Literal.String.Heredoc */
.codehilite .si {
color: #C41A16
}
/* Literal.String.Interpol */
/*mk: Literal.String.Interpol */
.codehilite .sx {
color: #C41A16
}
/* Literal.String.Other */
/*mk: Literal.String.Other */
.codehilite .sr {
color: #C41A16
}
/* Literal.String.Regex */
/*mk: Literal.String.Regex */
.codehilite .s1 {
color: #C41A16
}
/* Literal.String.Single */
/*mk: Literal.String.Single */
.codehilite .ss {
color: #C41A16
}
/* Literal.String.Symbol */
/*mk: Literal.String.Symbol */
.codehilite .bp {
color: #5B269A
}
/* Name.Builtin.Pseudo */
/*mk: Name.Builtin.Pseudo */
.codehilite .vc {
color: #000000
}
/* Name.Variable.Class */
/*mk: Name.Variable.Class */
.codehilite .vg {
color: #000000
}
/* Name.Variable.Global */
/*mk: Name.Variable.Global */
.codehilite .vi {
color: #000000
}
/* Name.Variable.Instance */
/*mk: Name.Variable.Instance */
.codehilite .il {
color: #1C01CE
}
/* Literal.Number.Integer.Long */
/*mk: Literal.Number.Integer.Long */

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save