11/08/2018, 20:12

Có thể bạn biết rồi: Cơ chế hoạt động của Syntax Highlighter

Nhắc đến Syntax Highlighter thì chẳng ai còn lạ gì nữa. Còn nếu bạn thấy lạ =)) thì đây là một công cụ giúp tô màu các đoạn mã nguồn trên trang web, ứng dụng của nó thì nhiều vô kể. Từ các code editor online như koding.com hay c9.io , codeanywhere , đến các plugin tô màu code cho các ...

Nhắc đến Syntax Highlighter thì chẳng ai còn lạ gì nữa.

Còn nếu bạn thấy lạ =)) thì đây là một công cụ giúp tô màu các đoạn mã nguồn trên trang web, ứng dụng của nó thì nhiều vô kể. Từ các code editor online như koding.com hay c9.io, codeanywhere, đến các plugin tô màu code cho các trang blog như wordpress hay Kipalog cũng có xài.

Đa số các plugin Syntax Highlighter này rất dễ tích hợp vào trang web của bạn, nhiều khi chỉ cần 1 file CSS, 1 file JS, thế là xong.

Nhưng đã bao giờ bạn tự hỏi là: Các plugin này hoạt động như thế nào? Và làm thế nào để tự viết một bộ Syntax Highlighter chưa?

Các bạn có thể tìm ra câu trả lời một cách dễ dàng bằng cách dùng Chrome Developer Tool hoặc Firebug để inspect vào cấu trúc HTML của một đoạn code được tô màu trên website, hoặc các editor online.

Còn nếu bạn lười quá, không muốn hoặc không biết cách inspect thì cứ đọc tiếp, mình sẽ giới thiệu luôn =))

Chúng ta đi vào xem xét từ ngoài vào trong, đầu tiên là cấu trúc HTML của một bộ highlighter. Lưu ý là bài viết trình bày tập trung vào các code editor có hỗ trợ highlighter, nhưng cấu trúc này đúng với cả các code highlighter trên các trang blog.

Cấu trúc HTML

Bí mật của Syntax Highlighter được gói gọn trong bức hình sau:

Như hình trên chúng ta thấy, có 2 lớp:

  • Lớp đầu tiên, nằm bên dưới có màu đậm hơn, đây là một textarea, nơi mà chúng ta tương tác để nhập code vào. Tạm gọi nó là code-input.
  • Lớp thứ 2, có màu sáng hơn, nằm đè lên trên, đây là một thẻ pre hoặc div tuỳ ý bạn, dùng để hiển thị code được tô màu lên trên, chúng ta gọi nó là code-output

Cấu trúc HTML của nó sẽ như thế này:

<div class='code-editor'>  
    <textarea class='code-input'></textarea>
    <pre class='code-output'></pre>
</div>  

Cả 2 lớp này đều có cùng 1 toạ độ, kích thước, font-family, font-size, line-height. Vì thế khi chúng đè lên nhau thì người dùng chỉ thấy có một textarea đồng nhất, vừa có thể gõ nội dung vào được, vừa thấy code được tô màu ngon lành. Đoạn CSS xử lý cho 2 lớp này như sau:

.code-editor {
    ...
    position: relative;
}

.code-input, .code-output {
    position: absolute;
    top: 0; left: 0;
    awidth: 100%; height: 100%;
    font-family: 'Monaco', monospace;
    font-size: 14px;
    line-height: 18px;
}

Xử lý input

Sau khi giải quyết được vấn đề "nghe nhìn" (2 lớp chồng lên nhau tạo hiệu ứng tô màu chữ), chúng ta đi vào xử lý vấn đề tương tác với textarea.

Như đã nói ở trên, chúng ta có lớp code-output đè lên code-input, vậy làm sao để chúng ta có thể click vào hoặc gõ vào code-input khi nó đã bị thằng khác đè lên? Chúng ta giải quyết vấn đề này bằng cách ngăn không cho người dùng tương tác với lớp code-output, thông qua thuộc tính pointer-events:

.code-output {
    ...
    pointer-events: none;
}

Bước tiếp theo, bây giờ chúng ta đã có thể tương tác được với code-input rồi, nhưng làm thế nào để cập nhật nội dung của code-output mỗi khi gõ vào code-input? Chúng t a cần bắt sự kiện nhấn phím hoặc đơn giản là sự kiện input của code-input. Nếu bạn bắt các sự kiện như keypress hay keyup, keydown, thì bạn cần tốn thời gian xử lý thêm cho trường hợp nhấn các phím đặc biệt như backspace để đồng bộ luôn việc xoá text.

var codeInput = document.querySelector('.code-input');  
var codeOutput = document.querySelector('.code-output');

codeInput.addEventListener('input', function(e){  
    var charCode = (typeof e.which == "number") ? e.which : e.keyCode;
    var newChar = String.fromCharCode(charCode);
    codeOutput.innerHTML = codeHighlight(codeInput.value + newChar);
});

Xử lý sự kiện scroll

Trong quá trình gõ code hoặc soạn thảo nội dung, chúng ta sẽ luôn thực hiện các thao tác scroll (cuộn) lên xuống, hoặc khi gõ enter xuống dòng, con trỏ tự động nhảy xuống dòng mới và trình soạn thảo cũng tự động scroll xuống. Như vậy chúng ta cần bắt sự kiện scroll của code-input để khi người dùng scroll, nó sẽ đồng bộ luôn việc scroll code-output (vì thằng output này đã bị chặn hoàn toàn mọi tương tác như đã nói ở phần trên rồi)

Code đơn giản như thế này thôi:

codeInput.addEventListener('scroll', function(e) {  
    codeOutput.scrollTop = codeInput.scrollTop;
});

Xử lý tô màu code (code highlighting)

Sau khi đã xử lý xong các thành phần cơ bản trên, bạn đã có một editor gần như hoàn chỉnh, chỉ cần một bước cuối cùng, là xử lý highlighting cho những đoạn code được gõ vào (để ý hàm codeHighlight() ở phần trên)

Vậy tô màu code là gì, tô màu ra sao? (nếu bạn thắc mắc). Giả sử chúng ta có đoạn nội dung của code-input như thế này:

// Hey yo!
function sum(a, b) {  
    return a + b;
}

Việc tô màu đoạn code trên, thực chất là chuyển đoạn code trên thành một đoạn nội dung HTML, mà các từ khoá được tô màu sẽ được gán cho một class nào đó để chúng ta có thể thực hiện việc styling cho nó thông qua CSS về sau. Ở ví dụ trên, nội dung của code-output sau khi thực hiện việc highlight sẽ thành ra thế này:

 
<span class='comment'>// Hey yo!</span>  
<span class='keyword'>function</span> sum(a, b) {  
    <span class='keyword'>return</span> a <span class='operator'>+</span> b;
}

Như vậy, chúng ta đã xác định là, các đoạn comment sẽ được gán cho class tên là comment, các từ khoá sẽ mang class keyword, các biểu thức toán học sẽ mang class operator, và chúng ta chỉ cần styling cho các class này trong CSS là xong.

Còn làm thế nào để chuyển từ đoạn code thông thường thành đoạn code style được bằng HTML? Chúng ta có rất nhiều cách.

Có thể dùng RegEx để xử lý tìm, replace, chèn các thẻ HTML vào:

output = input.replace(/function/g, '<span class="keyword">function</span>');
output = input.replace(/(+|-|*|/)/g, '<span class="operator">$1</span>');
...
codeOutput.innerHTML = output;

Cách này có lẽ rất khó với nhiều người, vì sẽ rất tốn thời gian, và cần thêm nhiều kiến thức về xử lý chuỗi, phân tích từ ngữ,... nhưng nếu làm được thì sẽ thu về 1 lượng kiến thức kha khá. Có thể tự mình custom để tạo ra các bộ highlight cho bất kì ngôn ngữ nào tuỳ ý.

Cách 2 đơn giản hơn, là dùng các bộ highlighter có sẵn để chuyển, ví dụ như Prism hay highlightJS, ví dụ:

output = hljs.highlight('javascript', input); 
codeOutput.innerHTML = output;

Cách này thì không cần tốn thời gian hay tí nơ ron thần kinh nào hết. Rất nhanh và gọn, tiện lợi, nhưng nhược điểm là bạn vẫn không biết cách tạo ra 1 bộ highlight ngôn ngữ cho riêng mình. Phụ thuộc hoàn toàn vào thư viện của người ta.

Như vậy là xong, hy vọng qua bài viết này, các bạn đã có thể hiểu rõ hơn cơ chế hoạt động của syntax highlighter. Bây giờ hãy thử tự xây dựng cho riêng một bộ highlighter/editor xem sao?

Nếu trong quá trình thực hiện, các bạn gặp vấn đề hoặc có câu hỏi, thì có thể tham gia thảo luận tại cộng đồng VietnamJS trên Slack (http://chat.vietnamjs.lol/) nhé :)

0