Skip to content

Commit 55460cd

Browse files
Soporte completo de Markdown (GFM) - FreeJS
- GitHub Flavored Markdown con kramdown - Preview server-side sin JavaScript - Ver codigo Markdown original - HTML raw permitido - Tablas normalizadas automaticamente - CSS dark theme para Markdown - Sidebar sticky con scroll StackRuby - Stack Overflow ligero, GPLv2 Funciona hasta en Emacs po!
1 parent 680020c commit 55460cd

17 files changed

Lines changed: 291 additions & 22 deletions

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ gem 'puma', '~> 6.0'
1313
gem 'sass-rails', '>= 6'
1414
# Use Active Model has_secure_password
1515
gem 'bcrypt', '~> 3.1.7'
16+
# Markdown parser
17+
gem 'kramdown', '~> 2.4'
18+
gem 'kramdown-parser-gfm', '~> 1.1'
1619

1720
group :development, :test do
1821
# Call 'byebug' anywhere in the code to stop execution and get a debugger console

Gemfile.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ GEM
7676
i18n (1.14.8)
7777
concurrent-ruby (~> 1.0)
7878
io-console (0.8.2)
79+
kramdown (2.5.2)
80+
rexml (>= 3.4.4)
81+
kramdown-parser-gfm (1.1.0)
82+
kramdown (~> 2.0)
7983
listen (3.10.0)
8084
logger
8185
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -149,6 +153,7 @@ GEM
149153
ffi (~> 1.0)
150154
reline (0.6.3)
151155
io-console (~> 0.5)
156+
rexml (3.4.4)
152157
sass-rails (6.0.0)
153158
sassc-rails (~> 2.1, >= 2.1.1)
154159
sassc (2.4.0)
@@ -185,6 +190,8 @@ PLATFORMS
185190
DEPENDENCIES
186191
bcrypt (~> 3.1.7)
187192
byebug
193+
kramdown (~> 2.4)
194+
kramdown-parser-gfm (~> 1.1)
188195
listen (~> 3.3)
189196
logger
190197
puma (~> 6.0)

app/controllers/comentarios_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ class ComentariosController < ApplicationController
33

44
def create
55
@preguntum = Preguntum.find(params[:preguntum_id])
6-
@comentario = @preguntum.comentarios.new(cuerpo: params[:cuerpo])
6+
@comentario = @preguntum.comentarios.new(cuerpo_markdown: params[:cuerpo_markdown])
77
@comentario.usuario_id = usuario_actual.id
88
@comentario.comentario_padre_id = params[:comentario_padre_id] if params[:comentario_padre_id].present?
99

@@ -20,7 +20,7 @@ def update
2020

2121
# Seguridad: Solo el autor real puede editar su propio comentario no-fantasma
2222
if usuario_actual && @comentario.usuario_id == usuario_actual.id && !@comentario.fantasma?
23-
if @comentario.update(cuerpo: params[:cuerpo])
23+
if @comentario.update(cuerpo_markdown: params[:cuerpo_markdown])
2424
redirect_to @comentario.preguntum, notice: "¡Comentario actualizado!"
2525
else
2626
redirect_to @comentario.preguntum, alert: "Error al actualizar el comentario."

app/controllers/pregunta_controller.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ def create
4545
@preguntum = Preguntum.new(preguntum_params)
4646
@preguntum.usuario_id = usuario_actual.id
4747

48-
if @preguntum.save
48+
# Si presionó el botón de preview
49+
if params[:preview]
50+
@preview_html = markdown_to_html(@preguntum.cuerpo_markdown)
51+
render :new, status: :unprocessable_entity
52+
elsif @preguntum.save
4953
redirect_to @preguntum, notice: "Pregunta creada con éxito."
5054
else
5155
render :new, status: :unprocessable_entity
@@ -54,7 +58,12 @@ def create
5458

5559
# PATCH/PUT /pregunta/1
5660
def update
57-
if @preguntum.update(preguntum_params)
61+
# Si presionó el botón de preview
62+
if params[:preview]
63+
@preguntum.assign_attributes(preguntum_params)
64+
@preview_html = markdown_to_html(@preguntum.cuerpo_markdown)
65+
render :edit, status: :unprocessable_entity
66+
elsif @preguntum.update(preguntum_params)
5867
redirect_to @preguntum, notice: "Pregunta actualizada."
5968
else
6069
render :edit, status: :unprocessable_entity
@@ -133,6 +142,22 @@ def set_preguntum
133142
end
134143

135144
def preguntum_params
136-
params.require(:preguntum).permit(:titulo, :cuerpo)
145+
params.require(:preguntum).permit(:titulo, :cuerpo_markdown)
146+
end
147+
148+
def markdown_to_html(text)
149+
return "" if text.blank?
150+
require 'kramdown'
151+
152+
# Normalizar tablas
153+
texto_normalizado = text.gsub(/\|[—–−]+\|/, '|---|')
154+
155+
options = {
156+
input: 'GFM',
157+
hard_wrap: true,
158+
parse_block_html: true,
159+
parse_span_html: true
160+
}
161+
Kramdown::Document.new(texto_normalizado, options).to_html.html_safe
137162
end
138163
end

app/helpers/markdown_helper.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'kramdown'
2+
3+
module MarkdownHelper
4+
def markdown_to_html(text)
5+
return "" if text.blank?
6+
7+
# Configuración de Kramdown
8+
options = {
9+
input: 'GFM', # GitHub Flavored Markdown
10+
hard_wrap: true, # Saltos de línea reales
11+
syntax_highlighter: nil, # Sin resaltado (para mantener simple)
12+
remove_block_html_tags: false, # Permitir HTML
13+
auto_ids: false # No generar IDs automáticos
14+
}
15+
16+
Kramdown::Document.new(text, options).to_html.html_safe
17+
end
18+
end

app/models/comentario.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class Comentario < ApplicationRecord
55
belongs_to :padre, class_name: "Comentario", foreign_key: "comentario_padre_id", optional: true
66
has_many :hijos, class_name: "Comentario", foreign_key: "comentario_padre_id", dependent: :destroy
77

8-
validates :cuerpo, presence: true
8+
validates :cuerpo_markdown, presence: true
9+
before_save :procesar_markdown
910

1011
def eliminar_con_poda!
1112
# Procedemos con la bifurcación del algoritmo de poda
@@ -24,6 +25,21 @@ def fantasma?
2425

2526
private
2627

28+
def procesar_markdown
29+
if cuerpo_markdown_changed?
30+
require 'kramdown'
31+
options = {
32+
input: 'GFM',
33+
hard_wrap: true,
34+
parse_block_html: true,
35+
parse_span_html: true
36+
}
37+
self.cuerpo_html = Kramdown::Document.new(cuerpo_markdown, options).to_html
38+
# Mantenemos 'cuerpo' sincronizado
39+
self.cuerpo = cuerpo_markdown
40+
end
41+
end
42+
2743
# Recolector en cascada reversa: Limpia padres fantasmas vacíos
2844
def recolector_fantasmas_reverso
2945
return unless padre.present?

app/models/preguntum.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ class Preguntum < ApplicationRecord
88
has_many :estrellas, dependent: :destroy
99
has_many :usuarios_que_dieron_estrella, through: :estrellas, source: :usuario
1010

11-
validates :titulo, :cuerpo, presence: true
11+
validates :titulo, presence: true
12+
validates :cuerpo_markdown, presence: true
13+
1214
before_create :inicializar_contadores
15+
before_save :procesar_markdown
1316

1417
# Método para contar cuántas estrellas tiene esta pregunta
1518
def total_estrellas
@@ -33,4 +36,23 @@ def inicializar_contadores
3336
self.votos ||= 0
3437
self.vistas ||= 0 if self.respond_to?(:vistas=)
3538
end
39+
40+
def procesar_markdown
41+
if cuerpo_markdown_changed?
42+
require 'kramdown'
43+
44+
# Normalizar tablas: reemplazar em-dash (—) y otros caracteres raros por guiones normales
45+
texto_normalizado = cuerpo_markdown.gsub(/\|[—–−]+\|/, '|---|')
46+
47+
options = {
48+
input: 'GFM',
49+
hard_wrap: true,
50+
parse_block_html: true, # Parsea bloques HTML
51+
parse_span_html: true # Parsea HTML inline
52+
}
53+
self.cuerpo_html = Kramdown::Document.new(texto_normalizado, options).to_html
54+
# Mantenemos 'cuerpo' sincronizado con el markdown por compatibilidad
55+
self.cuerpo = cuerpo_markdown
56+
end
57+
end
3658
end

app/views/comentarios/_comentario.html.erb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@
1313
<% end %>
1414
</p>
1515

16-
<p style="margin: 6px 0; color: var(--text-principal); font-size: 0.95em; white-space: pre-line; <%= 'font-style: italic; color: var(--text-secundario);' if comentario.fantasma? %>"><%= comentario.cuerpo %></p>
16+
<div style="margin: 6px 0; color: var(--text-principal); font-size: 0.95em; <%= 'font-style: italic; color: var(--text-secundario);' if comentario.fantasma? %>">
17+
<%= (comentario.cuerpo_html || comentario.cuerpo).html_safe %>
18+
</div>
1719

1820
<div style="display: flex; gap: 12px; font-size: 0.8em; align-items: center;">
1921
<% unless comentario.fantasma? %>
2022
<details style="display: inline;">
2123
<summary style="cursor: pointer; color: var(--text-secundario); list-style: none;">Responder</summary>
2224
<%= form_with url: preguntum_comentarios_path(comentario.preguntum), local: true, style: "margin-top: 8px; min-width: 250px;" do |f| %>
2325
<%= hidden_field_tag :comentario_padre_id, comentario.id %>
24-
<%= f.text_area :cuerpo, rows: 2, placeholder: "Escribe tu respuesta pública...", required: true, style: "margin-bottom: 8px;" %><br>
26+
<%= f.text_area :cuerpo_markdown, rows: 2, placeholder: "Escribe tu respuesta (Markdown soportado)...", required: true, style: "margin-bottom: 8px;" %><br>
2527
<%= f.submit "Responder", style: "padding: 6px 12px; font-size: 0.85em;" %>
2628
<% end %>
2729
</details>
@@ -35,7 +37,7 @@
3537
<summary style="cursor: pointer; color: var(--accent); list-style: none;">Editar</summary>
3638
<%# SOLUCIÓN POLIMÓRFICA: Rails deduce la ruta automáticamente %>
3739
<%= form_with model: [comentario.preguntum, comentario], method: :patch, local: true, style: "margin-top: 8px; min-width: 250px;" do |f| %>
38-
<%= f.text_area :cuerpo, value: comentario.cuerpo, rows: 2, required: true, style: "margin-bottom: 8px;" %><br>
40+
<%= f.text_area :cuerpo_markdown, value: comentario.cuerpo_markdown || comentario.cuerpo, rows: 2, required: true, style: "margin-bottom: 8px;" %><br>
3941
<%= f.submit "Guardar Cambios", style: "padding: 6px 12px; font-size: 0.85em;" %>
4042
<% end %>
4143
</details>

app/views/layouts/application.html.erb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,29 @@
4242
box-sizing: border-box;
4343
display: flex;
4444
flex-direction: column;
45+
position: sticky;
46+
top: 0;
47+
height: 100vh;
48+
overflow-y: auto;
49+
overflow-x: hidden;
50+
}
51+
52+
/* Estilos para el scrollbar del sidebar */
53+
.sidebar::-webkit-scrollbar {
54+
width: 8px;
55+
}
56+
57+
.sidebar::-webkit-scrollbar-track {
58+
background: var(--bg-tarjeta);
59+
}
60+
61+
.sidebar::-webkit-scrollbar-thumb {
62+
background: var(--border-color);
63+
border-radius: 4px;
64+
}
65+
66+
.sidebar::-webkit-scrollbar-thumb:hover {
67+
background: var(--text-secundario);
4568
}
4669

4770
/* Contenedor principal que usa TODO el ancho restante */
@@ -153,6 +176,89 @@
153176
object-fit: cover;
154177
border: 2px solid var(--accent);
155178
}
179+
180+
/* Estilos para contenido Markdown renderizado */
181+
h1, h2, h3, h4, h5, h6 {
182+
margin-top: 1.2em;
183+
margin-bottom: 0.6em;
184+
}
185+
186+
code {
187+
background: var(--bg-principal);
188+
padding: 2px 6px;
189+
border-radius: 3px;
190+
font-family: 'Courier New', monospace;
191+
font-size: 0.9em;
192+
color: #f97583;
193+
}
194+
195+
pre {
196+
background: var(--bg-tarjeta);
197+
padding: 12px;
198+
border-radius: 6px;
199+
overflow-x: auto;
200+
border: 1px solid var(--border-color);
201+
line-height: 1.2;
202+
}
203+
204+
pre code {
205+
background: none;
206+
padding: 0;
207+
color: var(--text-principal);
208+
font-family: 'Courier New', 'Consolas', monospace;
209+
}
210+
211+
blockquote {
212+
border-left: 3px solid var(--accent);
213+
padding-left: 15px;
214+
margin-left: 0;
215+
color: var(--text-secundario);
216+
font-style: italic;
217+
}
218+
219+
ul, ol {
220+
padding-left: 25px;
221+
line-height: 1.8;
222+
}
223+
224+
strong {
225+
color: white;
226+
font-weight: bold;
227+
}
228+
229+
em {
230+
font-style: italic;
231+
color: var(--accent);
232+
}
233+
234+
/* Estilos para tablas Markdown */
235+
table {
236+
border-collapse: collapse;
237+
width: 100%;
238+
margin: 20px 0;
239+
background: var(--bg-tarjeta);
240+
border: 1px solid var(--border-color);
241+
}
242+
243+
th, td {
244+
padding: 12px 15px;
245+
text-align: left;
246+
border: 1px solid var(--border-color);
247+
}
248+
249+
th {
250+
background: var(--bg-principal);
251+
font-weight: bold;
252+
color: white;
253+
}
254+
255+
tr:nth-child(even) {
256+
background: rgba(255, 255, 255, 0.02);
257+
}
258+
259+
tr:hover {
260+
background: rgba(88, 166, 255, 0.05);
261+
}
156262
</style>
157263
</head>
158264

app/views/pregunta/_form.html.erb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,24 @@
1616
</div>
1717

1818
<div class="field" style="margin-bottom: 15px;">
19-
<%= form.label :cuerpo, "Explica tu duda o problema con detalle", style: "display: block; margin-bottom: 5px; font-weight: bold;" %><br>
20-
<%= form.text_area :cuerpo, rows: 6, placeholder: "Introduce el contexto, código de error y lo que has intentado..." %>
19+
<%= form.label :cuerpo_markdown, "Explica tu duda o problema con detalle (Markdown soportado)", style: "display: block; margin-bottom: 5px; font-weight: bold;" %><br>
20+
<%= form.text_area :cuerpo_markdown, rows: 10, placeholder: "Introduce el contexto, código de error y lo que has intentado...\n\nEjemplo:\n# Título\n**Negrita** *cursiva*\n```ruby\ncodigo\n```" %>
21+
<p style="font-size: 0.85em; color: var(--text-secundario); margin-top: 5px;">
22+
Puedes usar Markdown: **negrita**, *cursiva*, `codigo`, ```bloques de codigo```, # Títulos, listas, etc.
23+
</p>
2124
</div>
2225

23-
<div class="actions">
24-
<%= form.submit preguntum.new_record? ? "Publicar Pregunta" : "Guardar Cambios" %>
26+
<% if defined?(@preview_html) && @preview_html.present? %>
27+
<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-principal); border: 1px solid var(--border-color); border-radius: 6px;">
28+
<h3 style="margin-top: 0; color: var(--accent);">Vista Previa:</h3>
29+
<div style="color: var(--text-principal); line-height: 1.6;">
30+
<%= @preview_html %>
31+
</div>
32+
</div>
33+
<% end %>
34+
35+
<div class="actions" style="display: flex; gap: 10px;">
36+
<%= form.submit "Preview", name: "preview", style: "background: var(--border-color); color: var(--text-principal);" %>
37+
<%= form.submit preguntum.new_record? ? "Publicar Pregunta" : "Guardar Cambios", name: "commit" %>
2538
</div>
26-
<% end %>
39+
<% end %>

0 commit comments

Comments
 (0)