---
title: # 標題
description: # 描述
image: # 封面
tags: # 內部標籤
robots: index, nofollow # [no]index, [no]follow
lang: zh # language
dir: ltr # left to right or right to left
breaks: # newline style
GA: UA-168653916-1 # google analysis
disqus: hackmd
slideOptions:
transition: fade # https://revealjs.com/transitions/
theme: back
---
# Linq Like Fluent Api
之所以叫做 Linq 'Like' 顧名思義 只是很像
以下會描述我在做 Linq Like 時遭遇到的問題
並且從中去了解 Linq 本身的某些功能是如何產生 或是 解決了什麼問題 如何解決的
~~這是事後諸葛論 而且不一定與當初開發時期相符 但具有其成為解決方法的合理性~~
## 如何實作 Fluent Api
首先先宣告一個可以提供計算結果的介面
這個介面主要是為了提供計算結果 ```Result```
泛型 ```T``` 則是結果型別
作法如下
```csharp
public interface ICalculator<T> where T : struct, IConvertible {
T Result { get; }
}
```
這裡實作 ```IConvertible``` 的原因是為了保證型別 ```T``` 是可以做值轉型 接下來就會用到
接著需要一個可以帶入起始值得進入點與建立產生進入點的方法
```csharp
internal class RootCalcuator<T> : ICalculator<T> where T : struct, IConvertible
{
private readonly T[] _values;
public RootCalcuator(T values) {
_values = values;
}
public T Result => _values;
}
public static ICalculator<R> Create<T>(T value)
where T : struct, IConvertible
=> new RootCalculator<R>(value);
```
接著建立擴充方法 Compute 讓使用者提供計算方式
```csharp
public static ICalculator<R> Compute<T, R>(this ICalculator<T> calculator, Func<T, R> func)
where T : struct, IConvertible
where R : struct, IConvertible
=> new ComputeCalculator<R>(func(calculator.Result));
internal class ComputeCalculator<T> : ICalculator<T>
where T : struct, IConvertible
where R : struct, IConvertible
{
public ComputeCalculator(T result) {
Result = result;
}
public R Result { get; }
}
```
到這邊為止看起來似乎都能符合我們這次的需求
除了 Linq 是處理多個對象 目前處理的是單一對象
而這個差別其實是相差非常遠的
如果我們簡單地把 ```T``` 變成 ```T[]```的時候
上述的程式碼將會即時的做完整個數組之後才往下一個步驟去
這意味著在此過程中 程式將會每有一個階段就快取一次結果
最終導致 ```OutOfMemoryException```
因此產生了一個需求
### 新任務: 是否能將一筆資料整個做完之後在處理下一筆
---
為何會有這種需求 因為一筆資料整個做完之後 或許他就不在了
例如可能寫入到檔案中
有些資料則可能在中途就可以過濾掉了
如果仍然留在程式中那本來就無法避免 OutOfMemory 的問題
~~意思就是把 OutOfMemory 的責任推給別人 不關我的事的概念 XD~~
#### 思考解決方法
在上述的作法 我們立即性的去計算結果後傳遞
或許我們可以不要這樣立即計算 而是當之後有需要的時候才計算結果
如果開始計算的時間 總是由最後一個步驟來啟動
那我們就可以在總是在最後一個步驟取到一個結果之後才做下一個
#### 實作解決方法
首先將 ```ICalculator<T>``` 改成能夠適應於上述的被動狀態
並且調整實作 ```RootCalculator<T>``` 使其能接受一整個數組
```csharp
public interface ICalculator<T> where T : struct, IConvertible {
bool Next();
T Current { get; }
}
internal class RootCalculator<T> : ICalculator<T> where T : struct, IConvertible
{
private readonly T[] _values;
private int _index = -1;
public RootCalcuator(T[] values) {
_values = values;
}
public T Current => _values[_index];
public bool Next()
=> ++_index <= _values.Length - 1;
}
```
你可能需要不同的 Calculator 類別來將不同的資料型態轉為 ```ICalculator<T>```
這裡只示範將陣列型別作為主要來源為起點的案例
如此一來他就是當被問是否有下一個值的時候 才會改變其```Current```成員的值
並且在沒有下一個值的時候 會以 ```Next``` 方法回傳 ```false```
接著從上述的結論 建立一個最後步驟 ```ToList``` 方法並且依照其規則來依序取得數組中每一個結果
```csharp
public static List<T> ToList<T>(this ICalculator<T> calculator)
where T : struct, IConvertible
{
var result = new List<T>();
while(calculator.Next()) {
result.Add(innerCalculator.Current);
}
return result;
}
```
接著要把 ```Compute``` 運算元也改成新的做法
前面我們於擴充方法中完成運算的這個做法必須延後到被存取時才做計算
這意味著我們在 ```ComputeCalculator<T>``` 必須要有第二個泛型參數來表達在此之前的值型別
```csharp
public static ICalculator<R> Compute<T, R>(this ICalculator<T> calculator, Func<T, R> func)
where T : struct, IConvertible
where R : struct, IConvertible
=> new ComputeCalculator<T, R>(calculator, func);
internal class ComputeCalculator<T, R> : ICalculator<R>
where T : struct, IConvertible
where R : struct, IConvertible
{
private readonly ICalculator<T> _last;
private readonly Func<T, R> _func;
public ComputeCalculator(ICalculator<T> last, Func<T, R> func) {
_last = last;
_func = func;
}
public R Current => _func(_last.Current);
public bool Next()
=> _last.Next();
}
```
透過從建構子帶入前一個階段的 ```ICalculator<T>``` 與使用者提供的運算方式 ```Func<T, R>``` 為參數
建立新的 ```ICalculator<R>``` 因為接下來並不需要關注 ```T``` 而是要關注 ```R``` 來接續建立階段
```這就是 Linq 的延遲執行的做法與行為方式```
```return yield 則只是在編譯時幫你產生 ComputeCalculator 這種類別的語法糖```
到這裡為止功能大致上已經完成的差不多了
但人們使用方式是難以想像的
有人將 ```ICalcaulator<T>``` 留存於變數 並且透過該變數存取多次而產生了多個分支
接著悲劇就發生了 因為同時多個分支都在存取 ```Next``` 導致其他分支出現跳號 遺漏元素等等的問題
以下為問題的範例
```
var seed = Enumerable.Range(1, 3).ToArray(); // create [1, 2, 3]
var root = Calculator.Create(seed).Compute(x => x + 1);
var first = root.Compute(x => root.Compute(y => x + y).List()[0]);
```
都完成到這裡了 勢必要想辦法解決這個問題
### 新任務: 能否在被存取第二次或更多次時 總是從頭開始產生新的過程
---
因為問題出自於最一開始被不同的結尾存取導致其他結尾被跳號的
所以 如果能夠保證每一次都能保證他自己是一條獨立的流程
被存取第二次時就會是新的一條流程提供給薪的分支接續
這樣問題就能夠排除了
#### 思考解決方法
前一次我們把值型計算留到最後才執行
這次我們也許可以把"建立整個流程"的觸發方式也放在最後一刻才開始
因為這樣做的話就表示 到最後一刻開始之前
沒有任何過程被建立 那在此時做出任何的分支行為都不會影響最後要建立的流程
#### 實踐解決方法
該如何讓最後一個階段才建立整個流程
我們勢必無法再直接操作 ```ICalculator<T>```來串接
因為在此時這個物件應該是還沒被建立的
而且 我們需要一個工廠來幫我們再最後建立 ```ICalculator<T>```
因此 把這兩件事情合起來產生一個新的介面 既可以做為工廠 也做為新的用於串接 api 的對象
```csharp
public interface ILinqLikeCalculator<T> where T : struct, IConvertible {
ICalculator<T> GetCalculator();
}
```
有了這個介面 接著把既有實作 ```ICalculator<T>``` 的類別
都額外實作 ```ILinqLikeCalculator<T>``` 並且讓 ```ICalculator<T>``` 的建立
移動到 ```GetCalculator``` 方法中
而且必須要在工廠方法被執行的時候才可以去跟前一個步驟拿 ```ICalculator<T>```
太早拿就會發生這次的問題
```csharp
internal class RootLinqLikeCalculator<T> : ILinqLikeCalculator<T> where T : struct, IConvertible {
private readonly T[] _values;
public RootLinqLikeCalculator(T[] values) {
_values = values;
}
public ICalculator<T> GetCalculator()
=> new RootCalculator<T>(_values);
}
internal class ComputeLinqLikeCalculator<T, R> : ILinqLikeCalculator<R>
where T : struct, IConvertible
where R : struct, IConvertible
{
private readonly ILinqLikeCalculator<T> _last;
private readonly Func<T, R> _func;
public ComputeLinqLikeCalculator(ILinqLikeCalculator<T> last, Func<T, R> func)
{
_last = last;
_func = func;
}
public ICalculator<R> GetCalculator()
=> new ComputeCalculator<T, R>(_last.GetCalculator(), _func);
}
```
並且修改建立階段的擴充方法 使其改為用 ```ILinqLikeCalculator<T>``` 來串連
```csharp
public static ILinqLikeCalculator<T> Create<T>(params T[] values) where T : struct, IConvertible
=> new RootLinqLikeCalculator<T>(values);
public static ILinqLikeCalculator<R> Compute<T, R>(
this ILinqLikeCalculator<T> calculator,
Func<T, R> func)
where T : struct, IConvertible
where R : struct, IConvertible
=> new ComputeLinqLikeCalculator<T, R>(calculator, func);
```
最後調整結尾的方法 使其改為透過 ```ILinqLikeCalculator<T>``` 來取得 ```ICalculator<T>```
並且以此 ```ICalculator<T>``` 來產生最後的結果
```csharp
public static List<T> ToList<T>(this ILinqLikeCalculator<T> calculator)
where T : struct, IConvertible
{
var result = new List<T>();
var innerCalculator = calculator.GetCalculator();
while(innerCalculator.Next()) {
result.Add(innerCalculator.Current);
}
return result;
}
```
### 新任務: 解決記憶體洩漏(Memory Leaking)
---
其實在程式領域是沒有偶發事件的
有哪些狀況會讓事情看起來像是偶發呢?
- 外部影響內部
- ex: 參數在預料之外
- 須累積而成
- ex: Memory Leak 而造成的記憶體不足
- 無紀錄的 handle 狀況
- throw empty, 或是 自動 restart 外加沒有人知道紀錄寫在事件檢視器 (IIS就會這樣做)
除了透過事後被這些問題搞到而學會之外
事前其實也還是可以多少推測出這些問題
以這次的案例來說
當我們找方法修正了分支造成的問題後
每一個流程在執行後 並不會真正的被回收
因為在一開始的地方永遠都會跟來源連結
而其他的分支也會跟來源連結而導致來源在全部分支都脫離前被釋放
因此這裡是一個很容易堆積已經用完的流程的地方
#### 思考解決方法
談到釋放那一定就得用 ```IDisposable``` 來解決了
在結尾事情完成之後 透過 ```Dispose``` 來告訴進入點可以脫離了
因此將 ```ICalculator<T>``` 加上 ```IDisposable```
並且在各個實作上追加實作
```csharp
public interface ICalculator<T> : IDisposable where T : struct, IConvertible {
bool Next();
T Current { get; }
}
internal sealed class RootCalculator<T> : ICalculator<T> where T : struct, IConvertible
{
private T[] _values;
private int _index = -1;
public RootCalculator(T[] values) {
_values = values;
}
public T Current => _values[_index];
public void Dispose()
=> _values = null;
public bool Next()
=> ++_index <= _values.Length - 1;
}
internal sealed class ComputeCalculator<T, R> : ICalculator<R>
where T : struct, IConvertible
where R : struct, IConvertible
{
private readonly ICalculator<T> _last;
private readonly Func<T, R> _func;
public ComputeCalculator(ICalculator<T> last, Func<T, R> func) {
_last = last;
_func = func;
}
public R Current => _func(_last.Current);
public void Dispose()
=> _last.Dispose();
public bool Next()
=> _last.Next();
}
```
這裡加上 sealed 的原因是避免他被繼承並引發在 ```Dispose``` 時
子類別也需要釋放資源而必須實作完整 Dispose Pattern (就是於自動產生時會問你並且產生較多方法的那個)
避免這件事情的原因是因為每一個運算員都應該只會運用自己的 Calculator
而一個運算元同時也只是代表一個動作 或一件事情
因此他本來就應該要是 sealed 只是之前是否必須要有 sealed 其實沒有差別
接著在結尾加上 ```using``` 關鍵字來使其方法結束時 執行 ```Dispose```
```csharp
public static List<T> ToList<T>(this ILinqLikeCalculator<T> calculator)
where T : struct, IConvertible
{
var result = new List<T>();
using var innerCalculator = calculator.GetCalculator();
while(innerCalculator.Next()) {
result.Add(innerCalculator.Current);
}
return result;
}
```
```using var```是新的語法
如果語言版本偏舊的請使用 ```using(var .....) { }```
## 總結
做到這裡應該有發現這個實現與 Linq 有非常多相似之處
像是物件結構非常的相似
|Linq|Calculator|
|-|-|
|IEnumerable<T>|ILinqLikeCalculator<T>|
|IEnumerable<T>.GetEnumerator()|ILinqLikeCalculator<T>.GetCalculator()|
|IEnumerator<T>|ICalculator<T>|
|IEnumerator<T>.Next()|ICalculator<T>.Next()|
|IEnumerator<T>.Current|ICalculator<T>.Current|
|IEnumerator<T>.Dispose()|ICalculator<T>.Dispose()|
|IEnumerable<T>.Select(...)|ILinqLikeCalculator<T>.Compute(...)|
|IEnumerable<T>.ToList()|ILinqLikeCalculator<T>.ToList()|
只差 Reset 方法沒有被提及 我想他勢必也有其存在的目的
因此這樣應該可以視為一種 Linq Like 的做法
並且依照這種對照方式 與實作過程
想要在 Linq 上追加新的 Operator 也應該不是難事了
[完整範例程式碼](https://github.com/dcvsling/fluent-api-example/tree/main/src/Calculator.LinqLike)
## 後記
其實我始終都覺得學習 Linq 用一個個 Operator 來解釋與嘗試的方式
只會讓人覺得東西很多 事情很複雜的感覺
最終讓人感覺有需要在用 最後乾脆以太複雜為由捨棄真的是很可惜
由上述內容可以得知 他並不是一個有限功能的工具
而是完全覆蓋所有可能的語法 因此應該是依照自己所想的流程
從裡面找符合的 Operator 甚至 Operator 組合來實現
因為沒有任何事情是既有的 Linq Operator 極其組合無法實現的
如果有 你也有辦法自行追加一個你需要的 Operator
之所以說他是一種語法 與一般的語法的差別就是在於 Iterator Pattern
它可以讓我們在思考過程時降低一個維度的複雜度
把需要思考做多次該如何做的事情
簡化成只需要思考做一次該如何做的事情
我想這也是為什麼會用 Linq 的人會講 No Linq Or Die 的原因了
~~包括我自己在內~~
<style>
body[style],
body[style*="background-color: white;"] {
background-color: #1e1e1e !important;
}
body {
color: #abb2bf;
}
.ui-view-area,
.markdown-body,
.ui-content {
background: #1e1e1e;
color: #abb2bf;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
color: #ddd;
}
hr {
border-color: #6d6d6d;
}
.form-control {
background: #333;
color: #fff;
}
.form-control::placeholder,
.form-control::-webkit-input-placeholder,
.form-control:-moz-placeholder,
.form-control::-moz-placeholder,
.form-control:-ms-input-placeholder {
color: #eee;
}
.header {
background-color: #0e0e0e;
border-color: #0e0e0e;
}
.navbar {
background-color: #0e0e0e;
border-color: #0e0e0e;
}
.navbar a {
color: #eee !important;
}
.navbar .btn-group label {
background-color: #0e0e0e;
color: #eee;
border-color: #555;
}
.navbar .btn-group label.btn-default:focus,
.navbar .btn-group label.btn-default:hover {
background-color: #2a2a2a;
color: #eee;
border-color: #555;
}
.navbar .btn-group label.active {
background-color: #555;
color: #eee;
border-color: #555;
}
.navbar .btn-group label.active:focus,
.navbar .btn-group label.active:hover {
background-color: #555;
color: #eee;
border-color: #555;
}
.navbar-default .btn-link:focus,
.navbar-default .btn-link:hover {
color: #eee;
}
.navbar-default .navbar-nav>.open>a,
.navbar-default .navbar-nav>.open>a:focus,
.navbar-default .navbar-nav>.open>a:hover {
background-color: #555;
}
.dropdown-header {
color: #aaa;
}
.dropdown-menu {
background-color: #222;
border: 1px solid #555;
border-top: none;
}
.dropdown-menu>li>a {
color: #eee;
}
.dropdown-menu>li>a:focus,
.dropdown-menu>li>a:hover {
background-color: #555555;
color: #eee;
}
.dropdown-menu .divider {
background-color: #555;
}
.header .open .dropdown-menu {
background-color: #202020;
}
.navbar .announcement-popover {
background: #4F4F4F;
}
.navbar .announcement-popover .announcement-popover-header {
background: #2e2e2e;
border-bottom: 1px solid #2e2e2e;
}
.navbar .announcement-popover .announcement-popover-body {
background: #4F4F4F;
color: #eee;
}
.navbar .announcement-popover .announcement-popover-footer {
background: #4F4F4F;
}
.navbar .announcement-area .caption.inverse {
color: #eee;
}
.label-warning {
background-color: #ffc107;
color: #212529;
}
.list.row-layout li .item {
border-color: #696c7d;
}
.list.row-layout li:nth-last-of-type(1) .item {
border-bottom: none;
}
.list li .item {
background: #1c1c1c;
}
.list li:hover .item,
.list li:focus .item {
background: #404040;
}
.list li .item h4 {
color: #fff;
}
.list li p {
color: #ccc;
}
.list li p i {
font-style: normal;
}
.list li .item .content .tags span {
background: #555;
}
.list li .item.wide .content .title a,
.list li .item.wide .content .title a:focus,
.list li .item.wide .content .title a:hover {
color: #ddd;
}
.ui-item {
color: #fff;
opacity: 0.7;
}
.ui-item:hover,
.ui-item:focus {
opacity: 1;
color: #fff;
}
.list li .item.wide hr {
border-color: #6d6d6d;
}
.overview-widget-group .btn,
.multi-select-dropdown-menu .ui-dropdown-label,
.multi-select-dropdown-menu .dropdown-options,
.form-control {
border-color: #6d6d6d;
}
.multi-select-dropdown-menu .dropdown-options .ui-option:hover {
background-color: #4d4d4d;
color: #eee;
}
#overview-control-form #overview-keyword-input-container .select2-container {
background-color: #3e4045 !important;
}
#overview-control-form #overview-keyword-input-container .select2-container .select2-choices {
background-color: #3e4045;
}
.search {
background-color: #3e4045;
color: #eee;
}
.btn.btn-gray {
background: #1b1b1b;
}
.btn.btn-gray:hover {
background: #4d4d4d;
color: #eee;
}
.search::placeholder,
.search::-webkit-input-placeholder,
.search:-moz-placeholder,
.search::-moz-placeholder,
.search:-ms-input-placeholder {
color: #eee;
}
.btn.btn-gray {
border-color: #6d6d6d;
background: #333;
color: #eee;
}
.select2-default {
color: #eee !important;
}
.select2-results .select2-highlighted {
background: #4d4d4d;
color: #eee;
}
.select2-container-multi .select2-choices {
background: #3e4045;
}
.select2-container-multi .select2-choices .select2-search-choice {
background: #131313;
color: #eee;
border-color: #555;
box-shadow: none;
}
.btn-default,
.btn-default:focus {
color: #eee;
background-color: #2e2e2e;
border-color: #6a6a6a;
}
.btn-default.active.focus,
.btn-default.active:focus,
.btn-default.active:hover,
.btn-default:active.focus,
.btn-default:active:focus,
.btn-default:active:hover,
.open>.dropdown-toggle.btn-default.focus,
.open>.dropdown-toggle.btn-default:focus,
.open>.dropdown-toggle.btn-default:hover {
background: #737373;
}
.btn-default:hover {
color: #fff;
background-color: #7d7d7d;
border-color: #6a6a6a;
}
.overview-widget-group .btn.active {
background-color: #6a6a6a;
color: #eee;
}
.overview-widget-group .btn:hover {
background-color: #7d7d7d;
color: #eee;
border-color: #636363;
}
.overview-widget-group .slider.round {
border-color: #ccc;
}
.overview-widget-group .slider.round:before {
border-color: #ccc;
}
.overview-widget-group input:checked+.slider {
background-color: #ccc;
}
.ui-category-description-icon a {
color: #eee;
}
.item .ui-history-pin.active {
color: #f00;
}
.ui-history-close {
color: #eee;
opacity: 0.5;
}
.pagination>li>a,
.pagination>li>span {
color: #eee;
background-color: #2e2e2e;
border-color: #6a6a6a;
}
.pagination>li>a:hover {
color: #fff;
background-color: #7d7d7d;
border-color: #6a6a6a;
}
.pagination>.disabled>a,
.pagination>.disabled>a:focus,
.pagination>.disabled>a:hover,
.pagination>.disabled>span,
.pagination>.disabled>span:focus,
.pagination>.disabled>span:hover {
color: #eee;
background-color: #2e2e2e;
border-color: #6a6a6a;
}
.pagination.dark>li>a,
.pagination.dark>li>span {
color: #aaa;
}
.section .form-horizontal .form-group .btn-default {
font-size: 16px;
border-color: #6d6d6d;
background-color: #333;
color: #FFF;
}
.section .form-horizontal .form-group .btn-default:hover,
.section .form-horizontal .form-group .btn-default:focus {
background-color: #737373;
color: #FFF;
}
.section .form-horizontal .form-control:focus {
border-color: #bbb;
}
#notificationLabel,
.ui-infobar .btn.ui-edit {
color: #eee;
border-color: #6a6a6a;
}
.ui-infobar__user-info li {
color: #bbb;
}
footer {
background: #101010;
color: #bbb;
border-top: 1px solid #454545;
}
footer a {
color: #bbb;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6,
.markdown-body hr,
#doc>h1 {
color: #ddd;
border-color: #777 !important;
}
.h1 .small,
.h1 small,
.h2 .small,
.h2 small,
.h3 .small,
.h3 small,
.h4 .small,
.h4 small,
.h5 .small,
.h5 small,
.h6 .small,
.h6 small,
h1 .small,
h1 small,
h2 .small,
h2 small,
h3 .small,
h3 small,
h4 .small,
h4 small,
h5 .small,
h5 small,
h6 .small,
h6 small {
color: #ddd;
}
.markdown-body p {
color: #ddd;
}
.markdown-body a {
color: #7bf;
}
.markdown-body a
{
color: #7bf !important;
}
.markdown-body ul li,
.markdown-body ol li {
color: #ddd;
}
.markdown-body blockquote {
color: #ddd;
border-left-color: #777;
font-size: 16px;
}
.markdown-body code,
code {
color: #dfdfdf !important;
background-color: #424a55;
}
.markdown-body pre {
background-color: #1e1e1e;
border: 1px solid #555 !important;
color: #dfdfdf;
}
blockquote .small,
blockquote footer,
blockquote small {
color: #bbb;
}
.mark,
mark {
background-color: rgba(255, 255, 0, 0.32) !important;
color: #ddd;
margin: .1em;
padding: .1em .2em;
}
.task-list-item-checkbox {
margin: 0.18em 0 0.2em -1.3em !important;
}
.task-list-item input[type=checkbox] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
position: relative;
top: -1px;
margin: 0 1rem 0 0;
cursor: pointer;
}
.task-list-item input[type=checkbox]::before {
-webkit-transition: all 0.1s ease-in-out;
-moz-transition: all 0.1s ease-in-out;
transition: all 0.1s ease-in-out;
content: "";
position: absolute;
left: 0;
z-index: 1;
width: 16px;
height: 16px;
border: 2px solid #F44336;
}
.task-list-item input[type=checkbox]:checked::before {
-webkit-transform: rotate(-48deg);
-moz-transform: rotate(-48deg);
-ms-transform: rotate(-48deg);
-o-transform: rotate(-48deg);
transform: rotate(-48deg);
height: 9px;
border-color: #00E676;
border-top-style: none;
border-right-style: none;
}
.task-list-item input[type=checkbox]::after {
content: "";
position: absolute;
top: -0.125rem;
left: 0;
width: 16px;
height: 16px;
background: #333;
cursor: pointer;
}
.markdown-body table tr {
background-color: #1e1e1e;
border-top: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.markdown-body table tr:first-child {
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.markdown-body table tr:nth-child(2n) {
background-color: #333;
}
.markdown-body table tr th {
color: #64B5F6;
}
.markdown-body table th,
.markdown-body table td {
border: none;
}
.markdown-body table tr th:first-child,
.markdown-body table tr td:first-child {
border-left: 1px solid rgba(255, 255, 255, 0.1);
}
.markdown-body table tr th:last-child,
.markdown-body table tr td:last-child {
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.markdown-body table tr td {
color: #ddd;
}
.markdown-body pre.flow-chart,
.markdown-body pre.sequence-diagram,
.markdown-body pre.graphviz,
.markdown-body pre.mermaid,
.markdown-body pre.abc {
background-color: #fff !important;
}
.alert h1,
.alert h2,
.alert h3,
.alert h4,
.alert h5,
.alert h6,
.alert p,
.alert ul li,
.alert ol li {
color: #31708f;
}
.alert a {
color: #002752;
font-weight: 700;
}
.alert h1:first-child,
.alert h2:first-child,
.alert h3:first-child,
.alert h4:first-child,
.alert h5:first-child,
.alert h6:first-child {
margin-top: 0;
}
.markdown-body .alert>p {
margin-top: 0px;
margin-bottom: 10px;
}
.markdown-body .alert>ul,
.markdown-body .alert>ol {
margin-bottom: 16px;
}
.markdown-body .alert>*:last-child {
margin-bottom: 0;
}
.alert-warning {
background-color: #fff3cd;
border-color: #ffeeba;
}
.ui-edit-area .ui-resizable-handle.ui-resizable-e {
background-color: #303030;
border: 1px solid #303030;
box-shadow: none;
}
.ui-infobar {
color: #999;
}
.permission-popover-btn-group .btn.focus,
.permission-popover-btn-group .btn:active,
.permission-popover-btn-group .btn:focus,
.permission-popover-btn-group .btn.active {
background-color: #6a6a6a !important;
color: #eee !important;
border-color: #555 !important;
}
.permission-popover-btn-group .btn:hover,
.permission-popover-btn-group .btn.active:hover {
background-color: #7d7d7d !important;
color: #eee !important;
border-color: #636363 !important;
}
.ui-delete-note a:hover,
.ui-delete-note a:focus,
.ui-delete-note a:active {
background-color: #dc3545 !important;
}
.ui-invitee-invite {
border-color: #6a6a6a !important;
}
.ui-invitee-invite:hover,
.ui-invitee-invite:focus {
background-color: #737373;
color: #eee !important;
}
.ui-invitee.ui-invitee-list .ui-invitee-remove,
.ui-invitee.ui-invitee-list .ui-invitee-remove:hover,
.ui-invitee.ui-invitee-list .ui-invitee-remove:focus,
.ui-invitee.ui-invitee-list .ui-invitee-remove:active {
background-color: #dc3545;
border: 1px solid #dc3545;
}
.select2-container {
background: #202020;
}
.select2-container-multi .select2-choices .select2-search-field input {
color: #eee;
}
.select2-container-multi .select2-choices .select2-search-field input.select2-active {
color: #000;
}
.select2-drop {
background: #202020;
color: #eee;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: #202020;
}
.ui-toc-dropdown {
width: 42vw;
max-height: 90vh;
overflow: auto;
text-align: inherit;
}
.ui-toc-dropdown .nav>li>a {
font-size: 14px;
font-weight: bold;
color: #ddd;
}
.ui-toc-dropdown .nav>.active:focus>a,
.ui-toc-dropdown .nav>.active:hover>a,
.ui-toc-dropdown .nav>.active>a {
color: #7bf;
border-left-color: #7bf;
}
.ui-toc-dropdown .nav>li>a:focus,
.ui-toc-dropdown .nav>li>a:hover {
color: #7bf;
border-left-color: #7bf;
}
.ui-toc-dropdown.dropdown-menu {
background: #333;
}
.toc-menu a {
color: #ddd;
}
.toc-menu a:focus,
.toc-menu a:hover {
color: #7bf;
}
.cm-m-markdown {
color: #ddd;
}
.cm-s-one-dark .cm-header,
.cm-m-xml.cm-attribute {
color: #ffa653;
}
.cm-s-one-dark .cm-string,
.cm-s-one-dark .cm-variable-2 {
color: #7bf;
}
.cm-m-markdown.cm-variable-3 {
color: #ff7e7e;
}
.cm-s-one-dark .cm-link {
color: #b0ee83;
}
.cm-s-one-dark .CodeMirror-linenumber {
color: #666;
}
.cm-strong {
color: #f4511e;
}
.cm-s-one-dark .cm-comment {
color: #a9a9a9;
}
.cm-matchhighlight {
color: #ffea00;
}
.cm-positive {
color: #11bf64;
}
.cm-negative {
color: #ff3e3e;
}
.dropdown-menu.CodeMirror-other-cursor {
border: 2px solid #4d4d4d;
background-color: #202020;
}
.dropdown-menu.CodeMirror-other-cursor li a {
color: #ececec;
}
.topbar {
background: #1e1e1e;
}
.btn.focus,
.btn:focus,
.btn:hover {
color: #aaa;
}
.summary {
background: #1e1e1e;
}
.summary,
.toolbar {
background: #1e1e1e !important;
border-color: #4d4d4d !important;
}
.toolbar i {
color: #fff;
}
.summary h1,
.summary h2,
.summary h3 .summary hr {
color: #ddd;
border-color: #777 !important;
}
.summary .nav>li>a {
color: #7bf;
}
.summary .nav-pills>li.active>a,
.summary .nav-pills>li.active>a:focus,
.summary .nav-pills>li.active>a:hover {
color: #ff9100;
}
.ui-summary-search {
font-size: 16px;
border: 1px solid #6D6D6D;
background-color: #333;
color: #FFF;
}
.summary h1,
.summary h2,
.summary h3,
.summary h4,
.summary h5,
.summary h6 {
border-color: #454545;
}
div[class$=container-mask] {
background: #1e1e1e;
z-index: 1;
display: block;
}
.dropdown.ui-notification .ui-notification-label,
.dropdown.ui-invitee .ui-invitee-label {
color: #eee;
border-color: #6a6a6a;
}
.ui-notification .dropdown-menu {
border-top: 1px solid #555;
}
.modal-header {
background-color: #2a2a2a;
}
.panel-default {
border-color: #6d6d6d;
}
.panel-default>.panel-heading {
background-color: #2a2a2a;
color: #eee;
border-color: #6d6d6d;
}
.panel-body {
background: #2e2e2e;
}
.panel-body a {
color: #7bf;
}
.table>tbody>tr>td,
.table>tbody>tr>th,
.table>tfoot>tr>td,
.table>tfoot>tr>th,
.table>thead>tr>td,
.table>thead>tr>th {
border-color: #6d6d6d;
}
.ui-comment-container .ui-comment-header {
background-color: #2a2a2a;
color: #eee;
border-color: #6d6d6d;
}
.ui-comment-container {
background-color: #2e2e2e;
border-color: #6d6d6d;
}
.ui-comment-container .ui-comments-container .ui-comment .comment-author {
color: #eee;
}
.ui-comment-container .ui-comments-container .ui-comment .timestamp {
color: #aaa;
}
.ui-comment-container .ui-comments-container .ui-comment .comment-content {
color: #eee;
}
.ui-comment-container .ui-comments-container .ui-comment .comment-menu {
color: #eee;
}
.ui-comment-container .ui-comments-container .ui-comment .comment-menu .comment-dropdown-menu {
background: #222;
color: #eee;
border-color: #555;
}
.ui-comment-container .ui-comments-container .ui-comment .comment-menu .comment-dropdown-menu>div:hover {
background-color: #555555;
color: #eee;
}
.ui-comment-container .ui-comments-container .ui-comment .comment-menu:hover,
.ui-comment-container .ui-comments-container .ui-comment .comment-menu:active,
.ui-comment-container .ui-comments-container .ui-comment .comment-menu.active {
background-color: #737373;
color: #eee;
}
.ui-comment-container .ui-comment-input-container {
background-color: #3c3c3c;
}
.ui-comment-container textarea {
background-color: #3e4045;
color: #eee;
border: 1px solid #6d6d6d;
}
.ui-comment-container textarea::placeholder,
.ui-comment-container textarea::-webkit-input-placeholder,
.ui-comment-container textarea:-moz-placeholder,
.ui-comment-container textarea::-moz-placeholder,
.ui-comment-container textarea:-ms-input-placeholder {
color: #eee;
}
@keyframes highlight {
0% {
background-color: #3c3c3c;
}
30% {
background-color: #3c3c3c;
}
100% {
background-color: transparent;
}
}
.template-content .modal-header {
background: #2a2a2a;
}
.template-content .close {
color: #fff;
}
.template-content .modal-title {
color: #eee;
}
.template-content .ui-templates-container {
border-color: #6d6d6d;
}
.ui-templates-container .ui-create-template-btn {
background: #446fab;
color: #fff;
}
.ui-template-list-filter .ui-template-list-filter-label,
.ui-template-list-filter .ui-template-list-filter-label:hover {
color: #eee;
}
.ui-template-list .list-group-item.active {
background: #4d4d4d;
}
.ui-template-list .list-group-item.active:focus {
background: #4d4d4d !important;
}
.list-group-item.active,
.list-group-item.active:focus,
.list-group-item.active:hover {
color: #eee;
}
.ui-template-list .list-group-item .list-group-item-heading {
color: #eee;
}
.ui-template-list .list-group-item.active .list-group-item-heading {
color: #eee;
}
.ui-template-list .list-group-item:hover {
background: #4d4d4d !important;
}
.ui-template-item-menu {
color: #eee !important;
}
.ui-template-list .list-group-item {
color: #fff;
}
.ui-template-list .list-group-item .dropdown-container.open {
background-color: #2a2a2a;
}
.ui-template-list .list-group-item .dropdown-container:hover {
background-color: #2a2a2a !important;
}
.template-menu .more-template {
border-color: #6d6d6d;
}
.template-menu .more-template:hover {
color: #eee;
border-color: #6d6d6d;
}
.modal-content {
background: #1f2226;
}
.modal-header {
border-bottom: 1px solid #46484f;
}
.modal-footer {
border-top: 1px solid #46484f;
}
a.list-group-item {
background: #1f2226;
color: #ddd;
border: 1px solid #46484f;
}
a.list-group-item .list-group-item-heading {
color: #ddd;
}
a.list-group-item:focus,
a.list-group-item:hover {
background: #434651;
color: #ddd;
}
button.close {
color: #ddd;
opacity: .5;
}
.close:focus, .close:hover {
color: #fff;
opacity: .8;
}
.CodeMirror {
background: #1f2226;
}
.CodeMirror-gutters {
background: #1f2226;
border-right: 1px solid rgba(204, 217, 255, 0.1);
}
.cm-s-default .cm-comment {
color: #888;
}
.cm-s-default .cm-quote {
color: #ddd;
}
.cm-s-default .cm-header {
color: #ffa653;
}
.cm-s-default .cm-link {
color: #b0ee83;
}
.cm-s-default .cm-string,
.cm-s-default .cm-variable-2 {
color: #7bf;
}
.cm-s-default .cm-def {
color: #c678dd;
}
.cm-s-default .cm-number,
.cm-s-default .cm-attribute,
.cm-s-default .cm-qualifier,
.cm-s-default .cm-plus,
.cm-s-default .cm-atom {
color: #eda35e;
}
.cm-s-default .cm-property,
.cm-s-default .cm-variable,
.cm-s-default .cm-variable-3,
.cm-s-default .cm-operator,
.cm-s-default .cm-bracket {
color: #f76e79;
}
.cm-s-default .cm-keyword,
.cm-s-default .cm-builtin,
.cm-s-default .cm-tag {
color: #98c379;
}
.modal-title {
color: #ccc;
}
.modal-body {
color: #ccc !important;
}
div[contenteditable]:empty:not(:focus):before {
color: #aaa;
}
.CodeMirror pre {
color: #ddd;
}
.CodeMirror pre span[style^="background-color: rgb(221, 251, 230)"] {
background-color: #288c27 !important;
}
.CodeMirror pre span[style^="background-color: rgb(249, 215, 220)"] {
background-color: #a52721 !important;
}
.hljs {
background: #1E1E1E;
color: #DCDCDC;
}
.hljs-keyword,
.hljs-literal,
.hljs-symbol,
.hljs-name {
color: #569CD6;
}
.hljs-link {
color: #569CD6;
text-decoration: underline;
}
.hljs-built_in,
.hljs-type {
color: #4EC9B0;
}
.hljs-number,
.hljs-class {
color: #B8D7A3;
}
.hljs-string,
.hljs-meta-string {
color: #D69D85;
}
.hljs-regexp,
.hljs-template-tag {
color: #d16969;
}
.hljs-title {
color: #dcdcaa;
}
.hljs-subst,
.hljs-function,
.hljs-formula {
color: #DCDCDC;
}
.hljs-comment,
.hljs-quote {
color: #57A64A;
}
.hljs-doctag {
color: #608B4E;
}
.hljs-meta,
.hljs-meta-keyword,
.hljs-tag {
color: #9B9B9B;
}
.hljs-variable,
.hljs-template-variable {
color: #BD63C5;
}
.hljs-params,
.hljs-attr,
.hljs-attribute,
.hljs-builtin-name {
color: #9CDCFE;
}
.hljs-section {
color: gold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-code {
font-family:'Monospace';
}
.hljs-bullet,
.hljs-selector-tag,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #D7BA7D;
}
.hljs-addition {
background-color: #155a36;
color: #dfdfdf;
display: inline-block;
width: 100%;
}
.hljs-deletion {
background-color: #872e2e;
color: #dfdfdf;
display: inline-block;
width: 100%;
}
code[class*="language-"],
pre[class*="language-"] {
color: #DCDCDC;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #1E1E1E;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.cdata {
color: #57A64A;
}
.token.doctype,
.token.punctuation {
color: #9B9B9B;
}
.token.tag,
.token.entity {
color: #569CD6;
}
.token.attr-name,
.token.namespace,
.token.deleted,
.token.property,
.token.builtin {
color: #9CDCFE;
}
.token.function,
.token.function-name {
color: #dcdcaa;
}
.token.boolean,
.token.keyword,
.token.important {
color: #569CD6;
}
.token.number {
color: #B8D7A3;
}
.token.class-name,
.token.constant {
color: #4EC9B0;
}
.token.symbol {
color: #f8c555;
}
.token.rule {
color: #c586c0;
}
.token.selector {
color: #D7BA7D;
}
.token.atrule {
color: #cc99cd;
}
.token.string,
.token.attr-value {
color: #D69D85;
}
.token.char {
color: #7ec699;
}
.token.variable {
color: #BD63C5;
}
.token.regex {
color: #d16969;
}
.token.operator {
color: #DCDCDC;
background-color: transparent;
}
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: green;
}
.gist .gist-file {
border: 1px solid #555;
}
.gist .gist-data {
background-color: #1e1e1e;
border-bottom: 1px solid #555;
}
.gist .gist-meta {
background-color: #424a55;
color: #eee;
}
.gist .gist-meta a {
color: #eee;
}
.gist .highlight {
color: #eee;
background-color: #1e1e1e;
}
.gist .blob-num {
color: #afafaf;
}
.gist .blob-code-inner {
color: #dfdfdf;
}
.pl-mb {
color: #fff !important;
}
.pl-c {
color: #57A64A !important;
}
.pl-ent {
color: #569CD6 !important;
}
.pl-e {
color: #9CDCFE !important;
}
.pl-en {
color: #4EC9B0 !important;
}
.pl-smi {
color: #9CDCFE !important;
}
.pl-k {
color: #569cd6 !important;
}
.pl-c1,
.pl-s .pl-v {
color: #4EC9B0 !important;
}
.pl-pds,
.pl-s,
.pl-s .pl-pse .pl-s1,
.pl-sr,
.pl-sr .pl-cce,
.pl-sr .pl-sra,
.pl-sr .pl-sre,
.pl-s .pl-s1 {
color: #D69D85 !important;
}
.pl-s .pl-s1 .pl-pse {
color: #c5dbff !important;
}
.diff-table .pl-c,
.diff-table .pl-ent,
.diff-table .pl-e,
.diff-table .pl-en,
.diff-table .pl-pds,
.diff-table .pl-s,
.diff-table .pl-s .pl-s1,
.diff-table .pl-s .pl-pse .pl-s1,
.diff-table .pl-sr,
.diff-table .pl-sr .pl-cce,
.diff-table .pl-sr .pl-sra,
.diff-table .pl-sr .pl-sre,
.diff-table .pl-k,
.diff-table .pl-smi,
.diff-table .pl-c1,
.diff-table .pl-v {
color: #eee !important;
}
</style>